diff options
author | Joerg Bornemann <joerg.bornemann@qt.io> | 2021-11-18 14:52:19 +0100 |
---|---|---|
committer | Joerg Bornemann <joerg.bornemann@qt.io> | 2021-11-23 21:11:45 +0100 |
commit | 3f56950862181f4d50f30d66f577c933795522c3 (patch) | |
tree | d6d47e2693605b3b207e22614860563ee606aff8 | |
parent | 1e9f9a4b7d1efa7efd5d501754f2de000a507cc5 (diff) |
Move macdeployqt and windeployqt from qttools to qtbase
Having all *deployqt tools in qtbase will allow us to couple deployment
support more tightly with the build system.
Change-Id: I299efdacfa6b66a303bb3996ff3ff84e723210a5
Reviewed-by: Kai Koehne <kai.koehne@qt.io>
29 files changed, 6791 insertions, 0 deletions
diff --git a/src/tools/CMakeLists.txt b/src/tools/CMakeLists.txt index 247b0193e6..77348abcc7 100644 --- a/src/tools/CMakeLists.txt +++ b/src/tools/CMakeLists.txt @@ -20,3 +20,11 @@ if(QT_FEATURE_androiddeployqt) add_subdirectory(androidtestrunner) endif() endif() + +if(QT_FEATURE_macdeployqt) + add_subdirectory(macdeployqt) +endif() + +if(QT_FEATURE_windeployqt) + add_subdirectory(windeployqt) +endif() diff --git a/src/tools/configure.cmake b/src/tools/configure.cmake index bee8a90ef7..8c474046ef 100644 --- a/src/tools/configure.cmake +++ b/src/tools/configure.cmake @@ -4,6 +4,18 @@ qt_feature("androiddeployqt" PRIVATE PURPOSE "The Android deployment tool automates the process of creating Android packages." CONDITION NOT CMAKE_CROSSCOMPILING AND QT_FEATURE_regularexpression) +qt_feature("macdeployqt" PRIVATE + SECTION "Deployment" + LABEL "macOS deployment tool" + PURPOSE "The Mac deployment tool automates the process of creating a deployable application bundle that contains the Qt libraries as private frameworks." + CONDITION MACOS) + +qt_feature("windeployqt" PRIVATE + SECTION "Deployment" + LABEL "Windows deployment tool" + PURPOSE "The Windows deployment tool is designed to automate the process of creating a deployable folder containing the Qt-related dependencies (libraries, QML imports, plugins, and translations) required to run the application from that folder. It creates a sandbox for Universal Windows Platform (UWP) or an installation tree for Windows desktop applications, which can be easily bundled into an installation package." + CONDITION WIN32) + qt_feature("qmake" PRIVATE PURPOSE "The qmake tool helps simplify the build process for development projects across different platforms." CONDITION QT_FEATURE_settings AND QT_FEATURE_alloca AND @@ -12,5 +24,7 @@ qt_feature("qmake" PRIVATE qt_configure_add_summary_section(NAME "Core tools") qt_configure_add_summary_entry(ARGS "androiddeployqt") +qt_configure_add_summary_entry(ARGS "macdeployqt") +qt_configure_add_summary_entry(ARGS "windeployqt") qt_configure_add_summary_entry(ARGS "qmake") qt_configure_end_summary_section() diff --git a/src/tools/macdeployqt/CMakeLists.txt b/src/tools/macdeployqt/CMakeLists.txt new file mode 100644 index 0000000000..cbce4e4ca3 --- /dev/null +++ b/src/tools/macdeployqt/CMakeLists.txt @@ -0,0 +1,6 @@ +# Generated from macdeployqt.pro. + +if(NOT QT_FEATURE_macdeployqt) + return() +endif() +add_subdirectory(macdeployqt) diff --git a/src/tools/macdeployqt/macdeployqt/CMakeLists.txt b/src/tools/macdeployqt/macdeployqt/CMakeLists.txt new file mode 100644 index 0000000000..85f117667d --- /dev/null +++ b/src/tools/macdeployqt/macdeployqt/CMakeLists.txt @@ -0,0 +1,16 @@ +##################################################################### +## macdeployqt Tool: +##################################################################### + +qt_get_tool_target_name(target_name macdeployqt) +qt_internal_add_tool(${target_name} + TOOLS_TARGET Core + USER_FACING + TARGET_DESCRIPTION "Qt macOS Deployment Tool" + SOURCES + ../shared/shared.cpp + main.cpp + LIBRARIES + ${FWCoreFoundation} +) +qt_internal_return_unless_building_tools() diff --git a/src/tools/macdeployqt/macdeployqt/main.cpp b/src/tools/macdeployqt/macdeployqt/main.cpp new file mode 100644 index 0000000000..145ee854af --- /dev/null +++ b/src/tools/macdeployqt/macdeployqt/main.cpp @@ -0,0 +1,278 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** 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 <QCoreApplication> +#include <QDir> +#include <QLibraryInfo> + +#include "../shared/shared.h" + +int main(int argc, char **argv) +{ + QCoreApplication app(argc, argv); + + QString appBundlePath; + if (argc > 1) + appBundlePath = QString::fromLocal8Bit(argv[1]); + + if (argc < 2 || appBundlePath.startsWith("-")) { + qDebug() << "Usage: macdeployqt app-bundle [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() << " -dmg : Create a .dmg disk image"; + qDebug() << " -no-strip : Don't run 'strip' on the binaries"; + qDebug() << " -use-debug-libs : Deploy with debug versions of frameworks and plugins (implies -no-strip)"; + qDebug() << " -executable=<path> : Let the given executable use the deployed frameworks too"; + qDebug() << " -qmldir=<path> : Scan for QML imports in the given path"; + qDebug() << " -qmlimport=<path> : Add the given path to the QML module search locations"; + qDebug() << " -always-overwrite : Copy files even if the target file exists"; + qDebug() << " -codesign=<ident> : Run codesign with the given identity on all executables"; + qDebug() << " -hardened-runtime : Enable Hardened Runtime when code signing"; + qDebug() << " -timestamp : Include a secure timestamp when code signing (requires internet connection)"; + qDebug() << " -sign-for-notarization=<ident>: Activate the necessary options for notarization (requires internet connection)"; + qDebug() << " -appstore-compliant : Skip deployment of components that use private API"; + qDebug() << " -libpath=<path> : Add the given path to the library search path"; + qDebug() << " -fs=<filesystem> : Set the filesystem used for the .dmg disk image (defaults to HFS+)"; + qDebug() << ""; + qDebug() << "macdeployqt takes an application bundle as input and makes it"; + qDebug() << "self-contained by copying in the Qt frameworks and plugins that"; + qDebug() << "the application uses."; + qDebug() << ""; + qDebug() << "Plugins related to a framework are copied in with the"; + qDebug() << "framework. The accessibility, image formats, and text codec"; + qDebug() << "plugins are always copied, unless \"-no-plugins\" is specified."; + qDebug() << ""; + qDebug() << "Qt plugins may use private API and will cause the app to be"; + qDebug() << "rejected from the Mac App store. MacDeployQt will print a warning"; + qDebug() << "when known incompatible plugins are deployed. Use -appstore-compliant "; + qDebug() << "to skip these plugins. Currently two SQL plugins are known to"; + qDebug() << "be incompatible: qsqlodbc and qsqlpsql."; + qDebug() << ""; + qDebug() << "See the \"Deploying Applications on OS X\" topic in the"; + qDebug() << "documentation for more information about deployment on OS X."; + + return 1; + } + + appBundlePath = QDir::cleanPath(appBundlePath); + + if (!QDir(appBundlePath).exists()) { + qDebug() << "Error: Could not find app bundle" << appBundlePath; + return 1; + } + + bool plugins = true; + bool dmg = false; + QByteArray filesystem("HFS+"); + bool useDebugLibs = false; + extern bool runStripEnabled; + extern bool alwaysOwerwriteEnabled; + extern QStringList librarySearchPath; + QStringList additionalExecutables; + bool qmldirArgumentUsed = false; + QStringList qmlDirs; + QStringList qmlImportPaths; + extern bool runCodesign; + extern QString codesignIdentiy; + extern bool hardenedRuntime; + extern bool appstoreCompliant; + extern bool deployFramework; + extern bool secureTimestamp; + + 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("-dmg")) { + LogDebug() << "Argument found:" << argument; + dmg = true; + } else if (argument == QByteArray("-no-strip")) { + LogDebug() << "Argument found:" << argument; + runStripEnabled = false; + } else if (argument == QByteArray("-use-debug-libs")) { + LogDebug() << "Argument found:" << argument; + useDebugLibs = true; + runStripEnabled = false; + } 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.startsWith(QByteArray("-qmlimport"))) { + LogDebug() << "Argument found:" << argument; + int index = argument.indexOf('='); + if (index == -1) + LogError() << "Missing qml import path"; + else + qmlImportPaths << argument.mid(index+1); + } else if (argument.startsWith(QByteArray("-libpath"))) { + LogDebug() << "Argument found:" << argument; + int index = argument.indexOf('='); + if (index == -1) + LogError() << "Missing library search path"; + else + librarySearchPath << argument.mid(index+1); + } else if (argument == QByteArray("-always-overwrite")) { + LogDebug() << "Argument found:" << argument; + alwaysOwerwriteEnabled = true; + } else if (argument.startsWith(QByteArray("-codesign"))) { + LogDebug() << "Argument found:" << argument; + int index = argument.indexOf("="); + if (index < 0 || index >= argument.size()) { + LogError() << "Missing code signing identity"; + } else { + runCodesign = true; + codesignIdentiy = argument.mid(index+1); + } + } else if (argument.startsWith(QByteArray("-sign-for-notarization"))) { + LogDebug() << "Argument found:" << argument; + int index = argument.indexOf("="); + if (index < 0 || index >= argument.size()) { + LogError() << "Missing code signing identity"; + } else { + runCodesign = true; + hardenedRuntime = true; + secureTimestamp = true; + codesignIdentiy = argument.mid(index+1); + } + } else if (argument.startsWith(QByteArray("-hardened-runtime"))) { + LogDebug() << "Argument found:" << argument; + hardenedRuntime = true; + } else if (argument.startsWith(QByteArray("-timestamp"))) { + LogDebug() << "Argument found:" << argument; + secureTimestamp = true; + } else if (argument == QByteArray("-appstore-compliant")) { + LogDebug() << "Argument found:" << argument; + appstoreCompliant = true; + + // Undocumented option, may not work as intended + } else if (argument == QByteArray("-deploy-framework")) { + LogDebug() << "Argument found:" << argument; + deployFramework = true; + + } else if (argument.startsWith(QByteArray("-fs"))) { + LogDebug() << "Argument found:" << argument; + int index = argument.indexOf('='); + if (index == -1) + LogError() << "Missing filesystem type"; + else + filesystem = argument.mid(index+1); + } else if (argument.startsWith("-")) { + LogError() << "Unknown argument" << argument << "\n"; + return 1; + } + } + + DeploymentInfo deploymentInfo = deployQtFrameworks(appBundlePath, additionalExecutables, useDebugLibs); + + if (deploymentInfo.isDebug) + useDebugLibs = true; + + if (deployFramework && deploymentInfo.isFramework) + fixupFramework(appBundlePath); + + // Convenience: Look for .qml files in the current directory if no -qmldir specified. + if (qmlDirs.isEmpty()) { + QDir dir; + if (!dir.entryList(QStringList() << QStringLiteral("*.qml")).isEmpty()) { + qmlDirs += QStringLiteral("."); + } + } + + if (!qmlDirs.isEmpty()) { + bool ok = deployQmlImports(appBundlePath, deploymentInfo, qmlDirs, qmlImportPaths); + if (!ok && qmldirArgumentUsed) + return 1; // exit if the user explicitly asked for qml import deployment + + // Update deploymentInfo.deployedFrameworks - the QML imports + // may have brought in extra frameworks as dependencies. + deploymentInfo.deployedFrameworks += findAppFrameworkNames(appBundlePath); + deploymentInfo.deployedFrameworks = + QSet<QString>(deploymentInfo.deployedFrameworks.begin(), + deploymentInfo.deployedFrameworks.end()).values(); + } + + // Handle plugins + if (plugins) { + // Set the plugins search directory + deploymentInfo.pluginPath = QLibraryInfo::path(QLibraryInfo::PluginsPath); + + // Sanity checks + if (deploymentInfo.pluginPath.isEmpty()) { + LogError() << "Missing Qt plugins path\n"; + return 1; + } + + if (!QDir(deploymentInfo.pluginPath).exists()) { + LogError() << "Plugins path does not exist" << deploymentInfo.pluginPath << "\n"; + return 1; + } + + // Deploy plugins + Q_ASSERT(!deploymentInfo.pluginPath.isEmpty()); + if (!deploymentInfo.pluginPath.isEmpty()) { + LogNormal(); + deployPlugins(appBundlePath, deploymentInfo, useDebugLibs); + createQtConf(appBundlePath); + } + } + + if (runStripEnabled) + stripAppBinary(appBundlePath); + + if (runCodesign) + codesign(codesignIdentiy, appBundlePath); + + if (dmg) { + LogNormal(); + createDiskImage(appBundlePath, filesystem); + } + + return 0; +} diff --git a/src/tools/macdeployqt/shared/shared.cpp b/src/tools/macdeployqt/shared/shared.cpp new file mode 100644 index 0000000000..b6eeaa8e80 --- /dev/null +++ b/src/tools/macdeployqt/shared/shared.cpp @@ -0,0 +1,1604 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** 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 <QCoreApplication> +#include <QString> +#include <QStringList> +#include <QDebug> +#include <iostream> +#include <utility> +#include <QProcess> +#include <QDir> +#include <QSet> +#include <QStack> +#include <QDirIterator> +#include <QLibraryInfo> +#include <QJsonDocument> +#include <QJsonObject> +#include <QJsonArray> +#include <QJsonValue> +#include <QRegularExpression> +#include "shared.h" + +#ifdef Q_OS_DARWIN +#include <CoreFoundation/CoreFoundation.h> +#endif + +bool runStripEnabled = true; +bool alwaysOwerwriteEnabled = false; +bool runCodesign = false; +QStringList librarySearchPath; +QString codesignIdentiy; +QString extraEntitlements; +bool hardenedRuntime = false; +bool secureTimestamp = false; +bool appstoreCompliant = false; +int logLevel = 1; +bool deployFramework = false; + +using std::cout; +using std::endl; + +bool operator==(const FrameworkInfo &a, const FrameworkInfo &b) +{ + return ((a.frameworkPath == b.frameworkPath) && (a.binaryPath == b.binaryPath)); +} + +QDebug operator<<(QDebug debug, const FrameworkInfo &info) +{ + debug << "Framework name" << info.frameworkName << "\n"; + debug << "Framework directory" << info.frameworkDirectory << "\n"; + debug << "Framework path" << info.frameworkPath << "\n"; + debug << "Binary directory" << info.binaryDirectory << "\n"; + debug << "Binary name" << info.binaryName << "\n"; + debug << "Binary path" << info.binaryPath << "\n"; + debug << "Version" << info.version << "\n"; + debug << "Install name" << info.installName << "\n"; + debug << "Deployed install name" << info.deployedInstallName << "\n"; + debug << "Source file Path" << info.sourceFilePath << "\n"; + debug << "Framework Destination Directory (relative to bundle)" << info.frameworkDestinationDirectory << "\n"; + debug << "Binary Destination Directory (relative to bundle)" << info.binaryDestinationDirectory << "\n"; + + return debug; +} + +const QString bundleFrameworkDirectory = "Contents/Frameworks"; + +inline QDebug operator<<(QDebug debug, const ApplicationBundleInfo &info) +{ + debug << "Application bundle path" << info.path << "\n"; + debug << "Binary path" << info.binaryPath << "\n"; + debug << "Additional libraries" << info.libraryPaths << "\n"; + return debug; +} + +bool copyFilePrintStatus(const QString &from, const QString &to) +{ + if (QFile::exists(to)) { + if (alwaysOwerwriteEnabled) { + QFile(to).remove(); + } else { + qDebug() << "File exists, skip copy:" << to; + 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; + } +} + +bool linkFilePrintStatus(const QString &file, const QString &link) +{ + if (QFile::exists(link)) { + if (QFile(link).symLinkTarget().isEmpty()) + LogError() << link << "exists but it's a file."; + else + LogNormal() << "Symlink exists, skipping:" << link; + return false; + } else if (QFile::link(file, link)) { + LogNormal() << " symlink" << link; + LogNormal() << " points to" << file; + return true; + } else { + LogError() << "failed to symlink" << link; + LogError() << " to" << file; + return false; + } +} + +void patch_debugInInfoPlist(const QString &infoPlistPath) +{ + // Older versions of qmake may have the "_debug" binary as + // the value for CFBundleExecutable. Remove it. + QFile infoPlist(infoPlistPath); + infoPlist.open(QIODevice::ReadOnly); + QByteArray contents = infoPlist.readAll(); + infoPlist.close(); + infoPlist.open(QIODevice::WriteOnly | QIODevice::Truncate); + contents.replace("_debug", ""); // surely there are no legit uses of "_debug" in an Info.plist + infoPlist.write(contents); +} + +OtoolInfo findDependencyInfo(const QString &binaryPath) +{ + OtoolInfo info; + info.binaryPath = binaryPath; + + LogDebug() << "Using otool:"; + LogDebug() << " inspecting" << binaryPath; + QProcess otool; + otool.start("otool", QStringList() << "-L" << binaryPath); + otool.waitForFinished(); + + if (otool.exitStatus() != QProcess::NormalExit || otool.exitCode() != 0) { + LogError() << otool.readAllStandardError(); + return info; + } + + static const QRegularExpression regexp(QStringLiteral( + "^\\t(.+) \\(compatibility version (\\d+\\.\\d+\\.\\d+), " + "current version (\\d+\\.\\d+\\.\\d+)(, weak)?\\)$")); + + QString output = otool.readAllStandardOutput(); + QStringList outputLines = output.split("\n", Qt::SkipEmptyParts); + if (outputLines.size() < 2) { + LogError() << "Could not parse otool output:" << output; + return info; + } + + outputLines.removeFirst(); // remove line containing the binary path + if (binaryPath.contains(".framework/") || binaryPath.endsWith(".dylib")) { + const auto match = regexp.match(outputLines.first()); + if (match.hasMatch()) { + QString installname = match.captured(1); + if (QFileInfo(binaryPath).fileName() == QFileInfo(installname).fileName()) { + info.installName = installname; + info.compatibilityVersion = QVersionNumber::fromString(match.captured(2)); + info.currentVersion = QVersionNumber::fromString(match.captured(3)); + outputLines.removeFirst(); + } else { + info.installName = binaryPath; + } + } else { + LogError() << "Could not parse otool output line:" << outputLines.first(); + outputLines.removeFirst(); + } + } + + for (const QString &outputLine : outputLines) { + const auto match = regexp.match(outputLine); + if (match.hasMatch()) { + DylibInfo dylib; + dylib.binaryPath = match.captured(1); + dylib.compatibilityVersion = QVersionNumber::fromString(match.captured(2)); + dylib.currentVersion = QVersionNumber::fromString(match.captured(3)); + info.dependencies << dylib; + } else { + LogError() << "Could not parse otool output line:" << outputLine; + } + } + + return info; +} + +FrameworkInfo parseOtoolLibraryLine(const QString &line, const QString &appBundlePath, const QList<QString> &rpaths, bool useDebugLibs) +{ + FrameworkInfo info; + QString trimmed = line.trimmed(); + + if (trimmed.isEmpty()) + return info; + + // Don't deploy system libraries. + if (trimmed.startsWith("/System/Library/") || + (trimmed.startsWith("/usr/lib/") && trimmed.contains("libQt") == false) // exception for libQtuitools and libQtlucene + || trimmed.startsWith("@executable_path") || trimmed.startsWith("@loader_path")) + return info; + + // Resolve rpath relative libraries. + if (trimmed.startsWith("@rpath/")) { + QString rpathRelativePath = trimmed.mid(QStringLiteral("@rpath/").length()); + bool foundInsideBundle = false; + for (const QString &rpath : std::as_const(rpaths)) { + QString path = QDir::cleanPath(rpath + "/" + rpathRelativePath); + // Skip paths already inside the bundle. + if (!appBundlePath.isEmpty()) { + if (QDir::isAbsolutePath(appBundlePath)) { + if (path.startsWith(QDir::cleanPath(appBundlePath) + "/")) { + foundInsideBundle = true; + continue; + } + } else { + if (path.startsWith(QDir::cleanPath(QDir::currentPath() + "/" + appBundlePath) + "/")) { + foundInsideBundle = true; + continue; + } + } + } + // Try again with substituted rpath. + FrameworkInfo resolvedInfo = parseOtoolLibraryLine(path, appBundlePath, rpaths, useDebugLibs); + if (!resolvedInfo.frameworkName.isEmpty() && QFile::exists(resolvedInfo.frameworkPath)) { + resolvedInfo.rpathUsed = rpath; + resolvedInfo.installName = trimmed; + return resolvedInfo; + } + } + if (!rpaths.isEmpty() && !foundInsideBundle) { + LogError() << "Cannot resolve rpath" << trimmed; + LogError() << " using" << rpaths; + } + return info; + } + + enum State {QtPath, FrameworkName, DylibName, Version, FrameworkBinary, End}; + State state = QtPath; + int part = 0; + QString name; + QString qtPath; + QString suffix = useDebugLibs ? "_debug" : ""; + + // Split the line into [Qt-path]/lib/qt[Module].framework/Versions/[Version]/ + QStringList parts = trimmed.split("/"); + while (part < parts.count()) { + const QString currentPart = parts.at(part).simplified(); + ++part; + if (currentPart == "") + continue; + + if (state == QtPath) { + // Check for library name part + if (part < parts.count() && parts.at(part).contains(".dylib")) { + info.frameworkDirectory += "/" + QString(qtPath + currentPart + "/").simplified(); + state = DylibName; + continue; + } else if (part < parts.count() && parts.at(part).endsWith(".framework")) { + info.frameworkDirectory += "/" + QString(qtPath + "lib/").simplified(); + state = FrameworkName; + continue; + } else if (trimmed.startsWith("/") == false) { // If the line does not contain a full path, the app is using a binary Qt package. + QStringList partsCopy = parts; + partsCopy.removeLast(); + for (QString &path : librarySearchPath) { + if (!path.endsWith("/")) + path += '/'; + QString nameInPath = path + parts.join(QLatin1Char('/')); + if (QFile::exists(nameInPath)) { + info.frameworkDirectory = path + partsCopy.join(QLatin1Char('/')); + break; + } + } + if (currentPart.contains(".framework")) { + if (info.frameworkDirectory.isEmpty()) + info.frameworkDirectory = "/Library/Frameworks/" + partsCopy.join(QLatin1Char('/')); + if (!info.frameworkDirectory.endsWith("/")) + info.frameworkDirectory += "/"; + state = FrameworkName; + --part; + continue; + } else if (currentPart.contains(".dylib")) { + if (info.frameworkDirectory.isEmpty()) + info.frameworkDirectory = "/usr/lib/" + partsCopy.join(QLatin1Char('/')); + if (!info.frameworkDirectory.endsWith("/")) + info.frameworkDirectory += "/"; + state = DylibName; + --part; + continue; + } + } + qtPath += (currentPart + "/"); + + } if (state == FrameworkName) { + // remove ".framework" + name = currentPart; + name.chop(QString(".framework").length()); + info.isDylib = false; + info.frameworkName = currentPart; + state = Version; + ++part; + continue; + } if (state == DylibName) { + name = currentPart; + info.isDylib = true; + info.frameworkName = name; + info.binaryName = name.contains(suffix) ? name : name.left(name.indexOf('.')) + suffix + name.mid(name.indexOf('.')); + info.deployedInstallName = "@executable_path/../Frameworks/" + info.binaryName; + info.frameworkPath = info.frameworkDirectory + info.binaryName; + info.sourceFilePath = info.frameworkPath; + info.frameworkDestinationDirectory = bundleFrameworkDirectory + "/"; + info.binaryDestinationDirectory = info.frameworkDestinationDirectory; + info.binaryDirectory = info.frameworkDirectory; + info.binaryPath = info.frameworkPath; + state = End; + ++part; + continue; + } else if (state == Version) { + info.version = currentPart; + info.binaryDirectory = "Versions/" + info.version; + info.frameworkPath = info.frameworkDirectory + info.frameworkName; + info.frameworkDestinationDirectory = bundleFrameworkDirectory + "/" + info.frameworkName; + info.binaryDestinationDirectory = info.frameworkDestinationDirectory + "/" + info.binaryDirectory; + state = FrameworkBinary; + } else if (state == FrameworkBinary) { + info.binaryName = currentPart.contains(suffix) ? currentPart : currentPart + suffix; + info.binaryPath = "/" + info.binaryDirectory + "/" + info.binaryName; + info.deployedInstallName = "@executable_path/../Frameworks/" + info.frameworkName + info.binaryPath; + info.sourceFilePath = info.frameworkPath + info.binaryPath; + state = End; + } else if (state == End) { + break; + } + } + + if (!info.sourceFilePath.isEmpty() && QFile::exists(info.sourceFilePath)) { + info.installName = findDependencyInfo(info.sourceFilePath).installName; + if (info.installName.startsWith("@rpath/")) + info.deployedInstallName = info.installName; + } + + return info; +} + +QString findAppBinary(const QString &appBundlePath) +{ + QString binaryPath; + +#ifdef Q_OS_DARWIN + CFStringRef bundlePath = appBundlePath.toCFString(); + CFURLRef bundleURL = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, bundlePath, + kCFURLPOSIXPathStyle, true); + CFRelease(bundlePath); + CFBundleRef bundle = CFBundleCreate(kCFAllocatorDefault, bundleURL); + if (bundle) { + CFURLRef executableURL = CFBundleCopyExecutableURL(bundle); + if (executableURL) { + CFURLRef absoluteExecutableURL = CFURLCopyAbsoluteURL(executableURL); + if (absoluteExecutableURL) { + CFStringRef executablePath = CFURLCopyFileSystemPath(absoluteExecutableURL, + kCFURLPOSIXPathStyle); + if (executablePath) { + binaryPath = QString::fromCFString(executablePath); + CFRelease(executablePath); + } + CFRelease(absoluteExecutableURL); + } + CFRelease(executableURL); + } + CFRelease(bundle); + } + CFRelease(bundleURL); +#endif + + if (QFile::exists(binaryPath)) + return binaryPath; + LogError() << "Could not find bundle binary for" << appBundlePath; + return QString(); +} + +QStringList findAppFrameworkNames(const QString &appBundlePath) +{ + QStringList frameworks; + + // populate the frameworks list with QtFoo.framework etc, + // as found in /Contents/Frameworks/ + QString searchPath = appBundlePath + "/Contents/Frameworks/"; + QDirIterator iter(searchPath, QStringList() << QString::fromLatin1("*.framework"), + QDir::Dirs | QDir::NoSymLinks); + while (iter.hasNext()) { + iter.next(); + frameworks << iter.fileInfo().fileName(); + } + + return frameworks; +} + +QStringList findAppFrameworkPaths(const QString &appBundlePath) +{ + QStringList frameworks; + QString searchPath = appBundlePath + "/Contents/Frameworks/"; + QDirIterator iter(searchPath, QStringList() << QString::fromLatin1("*.framework"), + QDir::Dirs | QDir::NoSymLinks); + while (iter.hasNext()) { + iter.next(); + frameworks << iter.fileInfo().filePath(); + } + + return frameworks; +} + +QStringList findAppLibraries(const QString &appBundlePath) +{ + QStringList result; + // dylibs + QDirIterator iter(appBundlePath, QStringList() << QString::fromLatin1("*.dylib"), + QDir::Files | QDir::NoSymLinks, QDirIterator::Subdirectories); + while (iter.hasNext()) { + iter.next(); + result << iter.fileInfo().filePath(); + } + return result; +} + +QStringList findAppBundleFiles(const QString &appBundlePath, bool absolutePath = false) +{ + QStringList result; + + QDirIterator iter(appBundlePath, QStringList() << QString::fromLatin1("*"), + QDir::Files, QDirIterator::Subdirectories); + + while (iter.hasNext()) { + iter.next(); + if (iter.fileInfo().isSymLink()) + continue; + result << (absolutePath ? iter.fileInfo().absoluteFilePath() : iter.fileInfo().filePath()); + } + + return result; +} + +QString findEntitlementsFile(const QString& path) +{ + QDirIterator iter(path, QStringList() << QString::fromLatin1("*.entitlements"), + QDir::Files, QDirIterator::Subdirectories); + + while (iter.hasNext()) { + iter.next(); + if (iter.fileInfo().isSymLink()) + continue; + + //return the first entitlements file - only one is used for signing anyway + return iter.fileInfo().absoluteFilePath(); + } + + return QString(); +} + +QList<FrameworkInfo> getQtFrameworks(const QList<DylibInfo> &dependencies, const QString &appBundlePath, const QList<QString> &rpaths, bool useDebugLibs) +{ + QList<FrameworkInfo> libraries; + for (const DylibInfo &dylibInfo : dependencies) { + FrameworkInfo info = parseOtoolLibraryLine(dylibInfo.binaryPath, appBundlePath, rpaths, useDebugLibs); + if (info.frameworkName.isEmpty() == false) { + LogDebug() << "Adding framework:"; + LogDebug() << info; + libraries.append(info); + } + } + return libraries; +} + +QString resolveDyldPrefix(const QString &path, const QString &loaderPath, const QString &executablePath) +{ + if (path.startsWith("@")) { + if (path.startsWith(QStringLiteral("@executable_path/"))) { + // path relative to bundle executable dir + if (QDir::isAbsolutePath(executablePath)) { + return QDir::cleanPath(QFileInfo(executablePath).path() + path.mid(QStringLiteral("@executable_path").length())); + } else { + return QDir::cleanPath(QDir::currentPath() + "/" + + QFileInfo(executablePath).path() + path.mid(QStringLiteral("@executable_path").length())); + } + } else if (path.startsWith(QStringLiteral("@loader_path"))) { + // path relative to loader dir + if (QDir::isAbsolutePath(loaderPath)) { + return QDir::cleanPath(QFileInfo(loaderPath).path() + path.mid(QStringLiteral("@loader_path").length())); + } else { + return QDir::cleanPath(QDir::currentPath() + "/" + + QFileInfo(loaderPath).path() + path.mid(QStringLiteral("@loader_path").length())); + } + } else { + LogError() << "Unexpected prefix" << path; + } + } + return path; +} + +QList<QString> getBinaryRPaths(const QString &path, bool resolve = true, QString executablePath = QString()) +{ + QList<QString> rpaths; + + QProcess otool; + otool.start("otool", QStringList() << "-l" << path); + otool.waitForFinished(); + + if (otool.exitCode() != 0) { + LogError() << otool.readAllStandardError(); + } + + if (resolve && executablePath.isEmpty()) { + executablePath = path; + } + + QString output = otool.readAllStandardOutput(); + QStringList outputLines = output.split("\n"); + + for (auto i = outputLines.cbegin(), end = outputLines.cend(); i != end; ++i) { + if (i->contains("cmd LC_RPATH") && ++i != end && + i->contains("cmdsize") && ++i != end) { + const QString &rpathCmd = *i; + int pathStart = rpathCmd.indexOf("path "); + int pathEnd = rpathCmd.indexOf(" ("); + if (pathStart >= 0 && pathEnd >= 0 && pathStart < pathEnd) { + QString rpath = rpathCmd.mid(pathStart + 5, pathEnd - pathStart - 5); + if (resolve) { + rpaths << resolveDyldPrefix(rpath, path, executablePath); + } else { + rpaths << rpath; + } + } + } + } + + return rpaths; +} + +QList<FrameworkInfo> getQtFrameworks(const QString &path, const QString &appBundlePath, const QList<QString> &rpaths, bool useDebugLibs) +{ + const OtoolInfo info = findDependencyInfo(path); + QList<QString> allRPaths = rpaths + getBinaryRPaths(path); + allRPaths.removeDuplicates(); + return getQtFrameworks(info.dependencies, appBundlePath, allRPaths, useDebugLibs); +} + +QList<FrameworkInfo> getQtFrameworksForPaths(const QStringList &paths, const QString &appBundlePath, const QList<QString> &rpaths, bool useDebugLibs) +{ + QList<FrameworkInfo> result; + QSet<QString> existing; + for (const QString &path : paths) { + for (const FrameworkInfo &info : getQtFrameworks(path, appBundlePath, rpaths, useDebugLibs)) { + if (!existing.contains(info.frameworkPath)) { // avoid duplicates + existing.insert(info.frameworkPath); + result << info; + } + } + } + return result; +} + +QStringList getBinaryDependencies(const QString executablePath, + const QString &path, + const QList<QString> &additionalBinariesContainingRpaths) +{ + QStringList binaries; + + const auto dependencies = findDependencyInfo(path).dependencies; + + bool rpathsLoaded = false; + QList<QString> rpaths; + + // return bundle-local dependencies. (those starting with @executable_path) + for (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) + rpaths += getBinaryRPaths(binaryPath, true); + rpaths.removeDuplicates(); + rpathsLoaded = true; + } + bool resolved = false; + for (const QString &rpath : std::as_const(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; + } + } + } + + return binaries; +} + +// copies everything _inside_ sourcePath to destinationPath +bool 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); + for (const 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); + for (const QString &dir : subdirs) { + recursiveCopy(sourcePath + "/" + dir, destinationPath + "/" + dir); + } + return true; +} + +void recursiveCopyAndDeploy(const QString &appBundlePath, const QList<QString> &rpaths, const QString &sourcePath, const QString &destinationPath) +{ + QDir().mkpath(destinationPath); + + LogNormal() << "copy:" << sourcePath << destinationPath; + const bool isDwarfPath = sourcePath.endsWith("DWARF"); + + QStringList files = QDir(sourcePath).entryList(QStringList() << QStringLiteral("*"), QDir::Files | QDir::NoDotAndDotDot); + for (const QString &file : files) { + const QString fileSourcePath = sourcePath + QLatin1Char('/') + file; + + if (file.endsWith("_debug.dylib")) { + continue; // Skip debug versions + } else if (!isDwarfPath && file.endsWith(QStringLiteral(".dylib"))) { + // App store code signing rules forbids code binaries in Contents/Resources/, + // which poses a problem for deploying mixed .qml/.dylib Qt Quick imports. + // Solve this by placing the dylibs in Contents/PlugIns/quick, and then + // creting a symlink to there from the Qt Quick import in Contents/Resources/. + // + // Example: + // MyApp.app/Contents/Resources/qml/QtQuick/Controls/libqtquickcontrolsplugin.dylib -> + // ../../../../PlugIns/quick/libqtquickcontrolsplugin.dylib + // + + // The .dylib destination path: + QString fileDestinationDir = appBundlePath + QStringLiteral("/Contents/PlugIns/quick/"); + QDir().mkpath(fileDestinationDir); + QString fileDestinationPath = fileDestinationDir + file; + + // The .dylib symlink destination path: + QString linkDestinationPath = destinationPath + QLatin1Char('/') + file; + + // The (relative) link; with a correct number of "../"'s. + QString linkPath = QStringLiteral("PlugIns/quick/") + file; + int cdupCount = linkDestinationPath.count(QStringLiteral("/")) - appBundlePath.count(QStringLiteral("/")); + for (int i = 0; i < cdupCount - 2; ++i) + linkPath.prepend("../"); + + if (copyFilePrintStatus(fileSourcePath, fileDestinationPath)) { + linkFilePrintStatus(linkPath, linkDestinationPath); + + runStrip(fileDestinationPath); + bool useDebugLibs = false; + bool useLoaderPath = false; + QList<FrameworkInfo> frameworks = getQtFrameworks(fileDestinationPath, appBundlePath, rpaths, useDebugLibs); + deployQtFrameworks(frameworks, appBundlePath, QStringList(fileDestinationPath), useDebugLibs, useLoaderPath); + } + } else { + QString fileDestinationPath = destinationPath + QLatin1Char('/') + file; + copyFilePrintStatus(fileSourcePath, fileDestinationPath); + } + } + + QStringList subdirs = QDir(sourcePath).entryList(QStringList() << QStringLiteral("*"), QDir::Dirs | QDir::NoDotAndDotDot); + for (const QString &dir : subdirs) { + recursiveCopyAndDeploy(appBundlePath, rpaths, sourcePath + QLatin1Char('/') + dir, destinationPath + QLatin1Char('/') + dir); + } +} + +QString copyDylib(const FrameworkInfo &framework, const QString path) +{ + if (!QFile::exists(framework.sourceFilePath)) { + LogError() << "no file at" << framework.sourceFilePath; + return QString(); + } + + // Construct destination paths. The full path typically looks like + // MyApp.app/Contents/Frameworks/libfoo.dylib + QString dylibDestinationDirectory = path + QLatin1Char('/') + framework.frameworkDestinationDirectory; + QString dylibDestinationBinaryPath = dylibDestinationDirectory + QLatin1Char('/') + framework.binaryName; + + // Create destination directory + if (!QDir().mkpath(dylibDestinationDirectory)) { + LogError() << "could not create destination directory" << dylibDestinationDirectory; + return QString(); + } + + // Return if the dylib has already been deployed + if (QFileInfo::exists(dylibDestinationBinaryPath) && !alwaysOwerwriteEnabled) + return dylibDestinationBinaryPath; + + // Copy dylib binary + copyFilePrintStatus(framework.sourceFilePath, dylibDestinationBinaryPath); + return dylibDestinationBinaryPath; +} + +QString copyFramework(const FrameworkInfo &framework, const QString path) +{ + if (!QFile::exists(framework.sourceFilePath)) { + LogError() << "no file at" << framework.sourceFilePath; + return QString(); + } + + // Construct destination paths. The full path typically looks like + // MyApp.app/Contents/Frameworks/Foo.framework/Versions/5/QtFoo + QString frameworkDestinationDirectory = path + QLatin1Char('/') + framework.frameworkDestinationDirectory; + QString frameworkBinaryDestinationDirectory = frameworkDestinationDirectory + QLatin1Char('/') + framework.binaryDirectory; + QString frameworkDestinationBinaryPath = frameworkBinaryDestinationDirectory + QLatin1Char('/') + framework.binaryName; + + // Return if the framework has aleardy been deployed + if (QDir(frameworkDestinationDirectory).exists() && !alwaysOwerwriteEnabled) + return QString(); + + // Create destination directory + if (!QDir().mkpath(frameworkBinaryDestinationDirectory)) { + LogError() << "could not create destination directory" << frameworkBinaryDestinationDirectory; + return QString(); + } + + // Now copy the framework. Some parts should be left out (headers/, .prl files). + // Some parts should be included (Resources/, symlink structure). We want this + // function to make as few assumptions about the framework as possible while at + // the same time producing a codesign-compatible framework. + + // Copy framework binary + copyFilePrintStatus(framework.sourceFilePath, frameworkDestinationBinaryPath); + + // Copy Resources/, Libraries/ and Helpers/ + const QString resourcesSourcePath = framework.frameworkPath + "/Resources"; + const QString resourcesDestianationPath = frameworkDestinationDirectory + "/Versions/" + framework.version + "/Resources"; + recursiveCopy(resourcesSourcePath, resourcesDestianationPath); + const QString librariesSourcePath = framework.frameworkPath + "/Libraries"; + const QString librariesDestianationPath = frameworkDestinationDirectory + "/Versions/" + framework.version + "/Libraries"; + bool createdLibraries = recursiveCopy(librariesSourcePath, librariesDestianationPath); + const QString helpersSourcePath = framework.frameworkPath + "/Helpers"; + const QString helpersDestianationPath = frameworkDestinationDirectory + "/Versions/" + framework.version + "/Helpers"; + bool createdHelpers = recursiveCopy(helpersSourcePath, helpersDestianationPath); + + // Create symlink structure. Links at the framework root point to Versions/Current/ + // which again points to the actual version: + // QtFoo.framework/QtFoo -> Versions/Current/QtFoo + // QtFoo.framework/Resources -> Versions/Current/Resources + // QtFoo.framework/Versions/Current -> 5 + linkFilePrintStatus("Versions/Current/" + framework.binaryName, frameworkDestinationDirectory + "/" + framework.binaryName); + linkFilePrintStatus("Versions/Current/Resources", frameworkDestinationDirectory + "/Resources"); + if (createdLibraries) + linkFilePrintStatus("Versions/Current/Libraries", frameworkDestinationDirectory + "/Libraries"); + if (createdHelpers) + linkFilePrintStatus("Versions/Current/Helpers", frameworkDestinationDirectory + "/Helpers"); + linkFilePrintStatus(framework.version, frameworkDestinationDirectory + "/Versions/Current"); + + // Correct Info.plist location for frameworks produced by older versions of qmake + // Contents/Info.plist should be Versions/5/Resources/Info.plist + const QString legacyInfoPlistPath = framework.frameworkPath + "/Contents/Info.plist"; + const QString correctInfoPlistPath = frameworkDestinationDirectory + "/Resources/Info.plist"; + if (QFile::exists(legacyInfoPlistPath)) { + copyFilePrintStatus(legacyInfoPlistPath, correctInfoPlistPath); + patch_debugInInfoPlist(correctInfoPlistPath); + } + return frameworkDestinationBinaryPath; +} + +void runInstallNameTool(QStringList options) +{ + QProcess installNametool; + installNametool.start("install_name_tool", options); + installNametool.waitForFinished(); + if (installNametool.exitCode() != 0) { + LogError() << installNametool.readAllStandardError(); + LogError() << installNametool.readAllStandardOutput(); + } +} + +void changeIdentification(const QString &id, const QString &binaryPath) +{ + LogDebug() << "Using install_name_tool:"; + LogDebug() << " change identification in" << binaryPath; + LogDebug() << " to" << id; + runInstallNameTool(QStringList() << "-id" << id << binaryPath); +} + +void changeInstallName(const QString &bundlePath, const FrameworkInfo &framework, const QStringList &binaryPaths, bool useLoaderPath) +{ + const QString absBundlePath = QFileInfo(bundlePath).absoluteFilePath(); + for (const QString &binary : binaryPaths) { + QString deployedInstallName; + if (useLoaderPath) { + deployedInstallName = QLatin1String("@loader_path/") + + QFileInfo(binary).absoluteDir().relativeFilePath(absBundlePath + QLatin1Char('/') + framework.binaryDestinationDirectory + QLatin1Char('/') + framework.binaryName); + } else { + deployedInstallName = framework.deployedInstallName; + } + changeInstallName(framework.installName, deployedInstallName, binary); + // Workaround for the case when the library ID name is a symlink, while the dependencies + // specified using the canonical path to the library (QTBUG-56814) + QString canonicalInstallName = QFileInfo(framework.installName).canonicalFilePath(); + if (!canonicalInstallName.isEmpty() && canonicalInstallName != framework.installName) { + changeInstallName(canonicalInstallName, deployedInstallName, binary); + } + } +} + +void addRPath(const QString &rpath, const QString &binaryPath) +{ + runInstallNameTool(QStringList() << "-add_rpath" << rpath << binaryPath); +} + +void deployRPaths(const QString &bundlePath, const QList<QString> &rpaths, const QString &binaryPath, bool useLoaderPath) +{ + const QString absFrameworksPath = QFileInfo(bundlePath).absoluteFilePath() + + QLatin1String("/Contents/Frameworks"); + const QString relativeFrameworkPath = QFileInfo(binaryPath).absoluteDir().relativeFilePath(absFrameworksPath); + const QString loaderPathToFrameworks = QLatin1String("@loader_path/") + relativeFrameworkPath; + bool rpathToFrameworksFound = false; + QStringList args; + QList<QString> binaryRPaths = getBinaryRPaths(binaryPath, false); + for (const QString &rpath : std::as_const(binaryRPaths)) { + if (rpath == "@executable_path/../Frameworks" || + rpath == loaderPathToFrameworks) { + rpathToFrameworksFound = true; + continue; + } + if (rpaths.contains(resolveDyldPrefix(rpath, binaryPath, binaryPath))) { + args << "-delete_rpath" << rpath; + } + } + if (!args.length()) { + return; + } + if (!rpathToFrameworksFound) { + if (!useLoaderPath) { + args << "-add_rpath" << "@executable_path/../Frameworks"; + } else { + args << "-add_rpath" << loaderPathToFrameworks; + } + } + LogDebug() << "Using install_name_tool:"; + LogDebug() << " change rpaths in" << binaryPath; + LogDebug() << " using" << args; + runInstallNameTool(QStringList() << args << binaryPath); +} + +void deployRPaths(const QString &bundlePath, const QList<QString> &rpaths, const QStringList &binaryPaths, bool useLoaderPath) +{ + for (const QString &binary : binaryPaths) { + deployRPaths(bundlePath, rpaths, binary, useLoaderPath); + } +} + +void changeInstallName(const QString &oldName, const QString &newName, const QString &binaryPath) +{ + LogDebug() << "Using install_name_tool:"; + LogDebug() << " in" << binaryPath; + LogDebug() << " change reference" << oldName; + LogDebug() << " to" << newName; + runInstallNameTool(QStringList() << "-change" << oldName << newName << binaryPath); +} + +void runStrip(const QString &binaryPath) +{ + if (runStripEnabled == false) + return; + + LogDebug() << "Using strip:"; + LogDebug() << " stripped" << binaryPath; + QProcess strip; + strip.start("strip", QStringList() << "-x" << binaryPath); + strip.waitForFinished(); + if (strip.exitCode() != 0) { + LogError() << strip.readAllStandardError(); + LogError() << strip.readAllStandardOutput(); + } +} + +void stripAppBinary(const QString &bundlePath) +{ + runStrip(findAppBinary(bundlePath)); +} + +bool DeploymentInfo::containsModule(const QString &module, const QString &libInFix) const +{ + // Check for framework first + if (deployedFrameworks.contains(QLatin1String("Qt") + module + libInFix + + QLatin1String(".framework"))) { + return true; + } + // Check for dylib + const QRegularExpression dylibRegExp(QLatin1String("libQt[0-9]+") + module + + libInFix + QLatin1String(".[0-9]+.dylib")); + return deployedFrameworks.filter(dylibRegExp).size() > 0; +} + +/* + Deploys the the listed frameworks listed into an app bundle. + The frameworks 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 frameworks. +*/ +DeploymentInfo deployQtFrameworks(QList<FrameworkInfo> frameworks, + const QString &bundlePath, const QStringList &binaryPaths, bool useDebugLibs, + bool useLoaderPath) +{ + LogNormal(); + LogNormal() << "Deploying Qt frameworks found inside:" << binaryPaths; + QStringList copiedFrameworks; + DeploymentInfo deploymentInfo; + deploymentInfo.useLoaderPath = useLoaderPath; + deploymentInfo.isFramework = bundlePath.contains(".framework"); + deploymentInfo.isDebug = false; + QList<QString> rpathsUsed; + + while (frameworks.isEmpty() == false) { + const FrameworkInfo framework = frameworks.takeFirst(); + copiedFrameworks.append(framework.frameworkName); + + // If a single dependency has the _debug suffix, we treat that as + // the whole deployment being a debug deployment, including deploying + // the debug version of plugins. + if (framework.isDebugLibrary()) + deploymentInfo.isDebug = true; + + if (deploymentInfo.qtPath.isNull()) + deploymentInfo.qtPath = QLibraryInfo::path(QLibraryInfo::PrefixPath); + + if (framework.frameworkDirectory.startsWith(bundlePath)) { + LogError() << framework.frameworkName << "already deployed, skipping."; + continue; + } + + if (!framework.rpathUsed.isEmpty() && !rpathsUsed.contains(framework.rpathUsed)) { + rpathsUsed.append(framework.rpathUsed); + } + + // Copy the framework/dylib to the app bundle. + const QString deployedBinaryPath = framework.isDylib ? copyDylib(framework, bundlePath) + : copyFramework(framework, bundlePath); + + // Install_name_tool the new id into the binaries + changeInstallName(bundlePath, framework, binaryPaths, useLoaderPath); + + // Skip the rest if already was deployed. + if (deployedBinaryPath.isNull()) + continue; + + runStrip(deployedBinaryPath); + + // Install_name_tool it a new id. + if (!framework.rpathUsed.length()) { + changeIdentification(framework.deployedInstallName, deployedBinaryPath); + } + + // Check for framework dependencies + QList<FrameworkInfo> dependencies = getQtFrameworks(deployedBinaryPath, bundlePath, rpathsUsed, useDebugLibs); + + for (const FrameworkInfo &dependency : dependencies) { + if (dependency.rpathUsed.isEmpty()) { + changeInstallName(bundlePath, dependency, QStringList() << deployedBinaryPath, useLoaderPath); + } else { + rpathsUsed.append(dependency.rpathUsed); + } + + // Deploy framework if necessary. + if (copiedFrameworks.contains(dependency.frameworkName) == false && frameworks.contains(dependency) == false) { + frameworks.append(dependency); + } + } + } + deploymentInfo.deployedFrameworks = copiedFrameworks; + deployRPaths(bundlePath, rpathsUsed, binaryPaths, useLoaderPath); + deploymentInfo.rpathsUsed += rpathsUsed; + deploymentInfo.rpathsUsed.removeDuplicates(); + return deploymentInfo; +} + +DeploymentInfo deployQtFrameworks(const QString &appBundlePath, const QStringList &additionalExecutables, bool useDebugLibs) +{ + ApplicationBundleInfo applicationBundle; + applicationBundle.path = appBundlePath; + applicationBundle.binaryPath = findAppBinary(appBundlePath); + applicationBundle.libraryPaths = findAppLibraries(appBundlePath); + QStringList allBinaryPaths = QStringList() << applicationBundle.binaryPath << applicationBundle.libraryPaths + << additionalExecutables; + + QList<QString> allLibraryPaths = getBinaryRPaths(applicationBundle.binaryPath, true); + allLibraryPaths.append(QLibraryInfo::path(QLibraryInfo::LibrariesPath)); + allLibraryPaths.removeDuplicates(); + + QList<FrameworkInfo> frameworks = getQtFrameworksForPaths(allBinaryPaths, appBundlePath, allLibraryPaths, useDebugLibs); + if (frameworks.isEmpty() && !alwaysOwerwriteEnabled) { + LogWarning(); + LogWarning() << "Could not find any external Qt frameworks to deploy in" << appBundlePath; + LogWarning() << "Perhaps macdeployqt was already used on" << appBundlePath << "?"; + LogWarning() << "If so, you will need to rebuild" << appBundlePath << "before trying again."; + return DeploymentInfo(); + } else { + return deployQtFrameworks(frameworks, applicationBundle.path, allBinaryPaths, useDebugLibs, !additionalExecutables.isEmpty()); + } +} + +QString getLibInfix(const QStringList &deployedFrameworks) +{ + QString libInfix; + for (const QString &framework : deployedFrameworks) { + if (framework.startsWith(QStringLiteral("QtCore")) && framework.endsWith(QStringLiteral(".framework")) && + !framework.contains(QStringLiteral("5Compat"))) { + Q_ASSERT(framework.length() >= 16); + // 16 == "QtCore" + ".framework" + const int lengthOfLibInfix = framework.length() - 16; + if (lengthOfLibInfix) + libInfix = framework.mid(6, lengthOfLibInfix); + break; + } + } + return libInfix; +} + +void deployPlugins(const ApplicationBundleInfo &appBundleInfo, const QString &pluginSourcePath, + const QString pluginDestinationPath, DeploymentInfo deploymentInfo, bool useDebugLibs) +{ + LogNormal() << "Deploying plugins from" << pluginSourcePath; + + if (!pluginSourcePath.contains(deploymentInfo.pluginPath)) + return; + + // Plugin white list: + QStringList pluginList; + + const auto addPlugins = [&pluginSourcePath,&pluginList,useDebugLibs](const QString &subDirectory, + const std::function<bool(QString)> &predicate = std::function<bool(QString)>()) { + const QStringList libs = QDir(pluginSourcePath + QLatin1Char('/') + subDirectory) + .entryList({QStringLiteral("*.dylib")}); + for (const QString &lib : libs) { + if (lib.endsWith(QStringLiteral("_debug.dylib")) != useDebugLibs) + continue; + if (!predicate || predicate(lib)) + pluginList.append(subDirectory + QLatin1Char('/') + lib); + } + }; + + // Platform plugin: + addPlugins(QStringLiteral("platforms"), [](const QString &lib) { + // Ignore minimal and offscreen platform plugins + if (!lib.contains(QStringLiteral("cocoa"))) + return false; + return true; + }); + + // Cocoa print support + addPlugins(QStringLiteral("printsupport")); + + // Styles + addPlugins(QStringLiteral("styles")); + + // Check if Qt was configured with -libinfix + const QString libInfix = getLibInfix(deploymentInfo.deployedFrameworks); + + // Network + if (deploymentInfo.containsModule("Network", libInfix)) + addPlugins(QStringLiteral("tls")); + + // All image formats (svg if QtSvg is used) + const bool usesSvg = deploymentInfo.containsModule("Svg", libInfix); + addPlugins(QStringLiteral("imageformats"), [usesSvg](const QString &lib) { + if (lib.contains(QStringLiteral("qsvg")) && !usesSvg) + return false; + return true; + }); + + addPlugins(QStringLiteral("iconengines")); + + // Platforminputcontext plugins if QtGui is in use + if (deploymentInfo.containsModule("Gui", libInfix)) { + addPlugins(QStringLiteral("platforminputcontexts"), [&addPlugins](const QString &lib) { + // Deploy the virtual keyboard plugins if we have deployed virtualkeyboard + if (lib.startsWith(QStringLiteral("libqtvirtualkeyboard"))) + addPlugins(QStringLiteral("virtualkeyboard")); + return true; + }); + } + + // Sql plugins if QtSql is in use + if (deploymentInfo.containsModule("Sql", libInfix)) { + addPlugins(QStringLiteral("sqldrivers"), [](const QString &lib) { + if (lib.startsWith(QStringLiteral("libqsqlodbc")) || lib.startsWith(QStringLiteral("libqsqlpsql"))) { + LogWarning() << "Plugin" << lib << "uses private API and is not Mac App store compliant."; + if (appstoreCompliant) { + LogWarning() << "Skip plugin" << lib; + return false; + } + } + return true; + }); + } + + // WebView plugins if QtWebView is in use + if (deploymentInfo.containsModule("WebView", libInfix)) { + addPlugins(QStringLiteral("webview"), [](const QString &lib) { + if (lib.startsWith(QStringLiteral("libqtwebview_webengine"))) { + LogWarning() << "Plugin" << lib << "uses QtWebEngine and is not Mac App store compliant."; + if (appstoreCompliant) { + LogWarning() << "Skip plugin" << lib; + return false; + } + } + return true; + }); + } + + static const std::map<QString, std::vector<QString>> map { + {QStringLiteral("Multimedia"), {QStringLiteral("mediaservice"), QStringLiteral("audio")}}, + {QStringLiteral("3DRender"), {QStringLiteral("sceneparsers"), QStringLiteral("geometryloaders"), QStringLiteral("renderers")}}, + {QStringLiteral("3DQuickRender"), {QStringLiteral("renderplugins")}}, + {QStringLiteral("Positioning"), {QStringLiteral("position")}}, + {QStringLiteral("Location"), {QStringLiteral("geoservices")}}, + {QStringLiteral("TextToSpeech"), {QStringLiteral("texttospeech")}} + }; + + for (const auto &it : map) { + if (deploymentInfo.containsModule(it.first, libInfix)) { + for (const auto &pluginType : it.second) { + addPlugins(pluginType); + } + } + } + + for (const QString &plugin : pluginList) { + QString sourcePath = pluginSourcePath + "/" + plugin; + const QString destinationPath = pluginDestinationPath + "/" + plugin; + QDir dir; + dir.mkpath(QFileInfo(destinationPath).path()); + + if (copyFilePrintStatus(sourcePath, destinationPath)) { + runStrip(destinationPath); + QList<FrameworkInfo> frameworks = getQtFrameworks(destinationPath, appBundleInfo.path, deploymentInfo.rpathsUsed, useDebugLibs); + deployQtFrameworks(frameworks, appBundleInfo.path, QStringList() << destinationPath, useDebugLibs, deploymentInfo.useLoaderPath); + } + } +} + +void createQtConf(const QString &appBundlePath) +{ + // Set Plugins and imports paths. These are relative to App.app/Contents. + QByteArray contents = "[Paths]\n" + "Plugins = PlugIns\n" + "Imports = Resources/qml\n" + "QmlImports = Resources/qml\n"; + + QString filePath = appBundlePath + "/Contents/Resources/"; + QString fileName = filePath + "qt.conf"; + + QDir().mkpath(filePath); + + QFile qtconf(fileName); + if (qtconf.exists() && !alwaysOwerwriteEnabled) { + LogWarning(); + LogWarning() << fileName << "already exists, will not overwrite."; + LogWarning() << "To make sure the plugins are loaded from the correct location,"; + LogWarning() << "please make sure qt.conf contains the following lines:"; + LogWarning() << "[Paths]"; + LogWarning() << " Plugins = PlugIns"; + return; + } + + qtconf.open(QIODevice::WriteOnly); + if (qtconf.write(contents) != -1) { + LogNormal() << "Created configuration file:" << fileName; + LogNormal() << "This file sets the plugin search path to" << appBundlePath + "/Contents/PlugIns"; + } +} + +void deployPlugins(const QString &appBundlePath, DeploymentInfo deploymentInfo, bool useDebugLibs) +{ + ApplicationBundleInfo applicationBundle; + applicationBundle.path = appBundlePath; + applicationBundle.binaryPath = findAppBinary(appBundlePath); + + const QString pluginDestinationPath = appBundlePath + "/" + "Contents/PlugIns"; + deployPlugins(applicationBundle, deploymentInfo.pluginPath, pluginDestinationPath, deploymentInfo, useDebugLibs); +} + +void deployQmlImport(const QString &appBundlePath, const QList<QString> &rpaths, const QString &importSourcePath, const QString &importName) +{ + QString importDestinationPath = appBundlePath + "/Contents/Resources/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(appBundlePath, rpaths, importSourcePath, importDestinationPath); +} + +static bool importLessThan(const QVariant &v1, const QVariant &v2) +{ + QVariantMap import1 = v1.toMap(); + QVariantMap import2 = v2.toMap(); + QString path1 = import1["path"].toString(); + QString path2 = import2["path"].toString(); + return path1 < path2; +} + +// Scan qml files in qmldirs for import statements, deploy used imports from QmlImportsPath to Contents/Resources/qml. +bool deployQmlImports(const QString &appBundlePath, DeploymentInfo deploymentInfo, QStringList &qmlDirs, QStringList &qmlImportPaths) +{ + LogNormal() << ""; + LogNormal() << "Deploying QML imports "; + LogNormal() << "Application QML file path(s) is" << qmlDirs; + LogNormal() << "QML module search path(s) is" << qmlImportPaths; + + // Use qmlimportscanner from QLibraryInfo::LibraryExecutablesPath + QString qmlImportScannerPath = + QDir::cleanPath(QLibraryInfo::path(QLibraryInfo::LibraryExecutablesPath) + + "/qmlimportscanner"); + + // Fallback: Look relative to the macdeployqt binary + if (!QFile::exists(qmlImportScannerPath)) + qmlImportScannerPath = QCoreApplication::applicationDirPath() + "/qmlimportscanner"; + + // Verify that we found a qmlimportscanner binary + if (!QFile::exists(qmlImportScannerPath)) { + LogError() << "qmlimportscanner not found at" << qmlImportScannerPath; + LogError() << "Rebuild qtdeclarative/tools/qmlimportscanner"; + return false; + } + + // 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; + for (const QString &qmlDir : qmlDirs) { + argumentList.append("-rootPath"); + argumentList.append(qmlDir); + } + for (const QString &importPath : qmlImportPaths) + argumentList << "-importPath" << importPath; + QString qmlImportsPath = QLibraryInfo::path(QLibraryInfo::QmlImportsPath); + argumentList.append( "-importPath"); + argumentList.append(qmlImportsPath); + + // run qmlimportscanner + QProcess qmlImportScanner; + qmlImportScanner.start(qmlImportScannerPath, argumentList); + if (!qmlImportScanner.waitForStarted()) { + LogError() << "Could not start qmlimpoortscanner. Process error is" << qmlImportScanner.errorString(); + return false; + } + qmlImportScanner.waitForFinished(-1); + + // log qmlimportscanner errors + qmlImportScanner.setReadChannel(QProcess::StandardError); + QByteArray errors = qmlImportScanner.readAll(); + if (!errors.isEmpty()) { + LogWarning() << "QML file parse error (deployment will continue):"; + LogWarning() << errors; + } + + // parse qmlimportscanner json + qmlImportScanner.setReadChannel(QProcess::StandardOutput); + QByteArray json = qmlImportScanner.readAll(); + QJsonDocument doc = QJsonDocument::fromJson(json); + if (!doc.isArray()) { + LogError() << "qmlimportscanner output error. Expected json array, got:"; + LogError() << json; + return false; + } + + // sort imports to deploy a module before its sub-modules (otherwise + // deployQmlImports can consider the module deployed if it has already + // deployed one of its sub-module) + QVariantList array = doc.array().toVariantList(); + std::sort(array.begin(), array.end(), importLessThan); + + // deploy each import + for (const QVariant &importValue : array) { + QVariantMap import = importValue.toMap(); + QString name = import["name"].toString(); + QString path = import["path"].toString(); + QString type = import["type"].toString(); + + LogNormal() << "Deploying QML import" << name; + + // Skip imports with missing info - path will be empty if the import is not found. + if (name.isEmpty() || path.isEmpty()) { + LogNormal() << " Skip import: name or path is empty"; + LogNormal() << ""; + continue; + } + + // Deploy module imports only, skip directory (local/remote) and js imports. These + // should be deployed as a part of the application build. + if (type != QStringLiteral("module")) { + LogNormal() << " Skip non-module import"; + LogNormal() << ""; + continue; + } + + // Create the destination path from the name + // and version (grabbed from the source path) + // ### let qmlimportscanner provide this. + name.replace(QLatin1Char('.'), QLatin1Char('/')); + int secondTolast = path.length() - 2; + QString version = path.mid(secondTolast); + if (version.startsWith(QLatin1Char('.'))) + name.append(version); + + deployQmlImport(appBundlePath, deploymentInfo.rpathsUsed, path, name); + LogNormal() << ""; + } + return true; +} + +void codesignFile(const QString &identity, const QString &filePath) +{ + if (!runCodesign) + return; + + QString codeSignLogMessage = "codesign"; + if (hardenedRuntime) + codeSignLogMessage += ", enable hardened runtime"; + if (secureTimestamp) + codeSignLogMessage += ", include secure timestamp"; + LogNormal() << codeSignLogMessage << filePath; + + QStringList codeSignOptions = { "--preserve-metadata=identifier,entitlements", "--force", "-s", + identity, filePath }; + if (hardenedRuntime) + codeSignOptions << "-o" << "runtime"; + + if (secureTimestamp) + codeSignOptions << "--timestamp"; + + if (!extraEntitlements.isEmpty()) + codeSignOptions << "--entitlements" << extraEntitlements; + + QProcess codesign; + codesign.start("codesign", codeSignOptions); + codesign.waitForFinished(-1); + + QByteArray err = codesign.readAllStandardError(); + if (codesign.exitCode() > 0) { + LogError() << "Codesign signing error:"; + LogError() << err; + } else if (!err.isEmpty()) { + LogDebug() << err; + } +} + +QSet<QString> codesignBundle(const QString &identity, + const QString &appBundlePath, + QList<QString> additionalBinariesContainingRpaths) +{ + // Code sign all binaries in the app bundle. This needs to + // be done inside-out, e.g sign framework dependencies + // before the main app binary. The codesign tool itself has + // a "--deep" option to do this, but usage when signing is + // not recommended: "Signing with --deep is for emergency + // repairs and temporary adjustments only." + + LogNormal() << ""; + LogNormal() << "Signing" << appBundlePath << "with identity" << identity; + + QStack<QString> pendingBinaries; + QSet<QString> pendingBinariesSet; + QSet<QString> signedBinaries; + + // Create the root code-binary set. This set consists of the application + // executable(s) and the plugins. + QString appBundleAbsolutePath = QFileInfo(appBundlePath).absoluteFilePath(); + QString rootBinariesPath = appBundleAbsolutePath + "/Contents/MacOS/"; + QStringList foundRootBinaries = QDir(rootBinariesPath).entryList(QStringList() << "*", QDir::Files); + for (const QString &binary : foundRootBinaries) { + QString binaryPath = rootBinariesPath + binary; + pendingBinaries.push(binaryPath); + pendingBinariesSet.insert(binaryPath); + additionalBinariesContainingRpaths.append(binaryPath); + } + + bool getAbsoltuePath = true; + QStringList foundPluginBinaries = findAppBundleFiles(appBundlePath + "/Contents/PlugIns/", getAbsoltuePath); + for (const QString &binary : foundPluginBinaries) { + pendingBinaries.push(binary); + pendingBinariesSet.insert(binary); + } + + // Add frameworks for processing. + QStringList frameworkPaths = findAppFrameworkPaths(appBundlePath); + for (const QString &frameworkPath : frameworkPaths) { + + // Prioritise first to sign any additional inner bundles found in the Helpers folder (e.g + // used by QtWebEngine). + QDirIterator helpersIterator(frameworkPath, QStringList() << QString::fromLatin1("Helpers"), QDir::Dirs | QDir::NoSymLinks, QDirIterator::Subdirectories); + while (helpersIterator.hasNext()) { + helpersIterator.next(); + QString helpersPath = helpersIterator.filePath(); + QStringList innerBundleNames = QDir(helpersPath).entryList(QStringList() << "*.app", QDir::Dirs); + for (const QString &innerBundleName : innerBundleNames) + signedBinaries += codesignBundle(identity, + helpersPath + "/" + innerBundleName, + additionalBinariesContainingRpaths); + } + + // Also make sure to sign any libraries that will not be found by otool because they + // are not linked and won't be seen as a dependency. + QDirIterator librariesIterator(frameworkPath, QStringList() << QString::fromLatin1("Libraries"), QDir::Dirs | QDir::NoSymLinks, QDirIterator::Subdirectories); + while (librariesIterator.hasNext()) { + librariesIterator.next(); + QString librariesPath = librariesIterator.filePath(); + QStringList bundleFiles = findAppBundleFiles(librariesPath, getAbsoltuePath); + for (const QString &binary : bundleFiles) { + pendingBinaries.push(binary); + pendingBinariesSet.insert(binary); + } + } + } + + // Sign all binaries; use otool to find and sign dependencies first. + while (!pendingBinaries.isEmpty()) { + QString binary = pendingBinaries.pop(); + if (signedBinaries.contains(binary)) + continue; + + // Check if there are unsigned dependencies, sign these first. + QStringList dependencies = getBinaryDependencies(rootBinariesPath, binary, + additionalBinariesContainingRpaths); + dependencies = QSet<QString>(dependencies.begin(), dependencies.end()) + .subtract(signedBinaries) + .subtract(pendingBinariesSet) + .values(); + + if (!dependencies.isEmpty()) { + pendingBinaries.push(binary); + pendingBinariesSet.insert(binary); + int dependenciesSkipped = 0; + for (const QString &dependency : std::as_const(dependencies)) { + // Skip dependencies that are outside the current app bundle, because this might + // cause a codesign error if the current bundle is part of the dependency (e.g. + // a bundle is part of a framework helper, and depends on that framework). + // The dependencies will be taken care of after the current bundle is signed. + if (!dependency.startsWith(appBundleAbsolutePath)) { + ++dependenciesSkipped; + LogNormal() << "Skipping outside dependency: " << dependency; + continue; + } + pendingBinaries.push(dependency); + pendingBinariesSet.insert(dependency); + } + + // If all dependencies were skipped, make sure the binary is actually signed, instead + // of going into an infinite loop. + if (dependenciesSkipped == dependencies.size()) { + pendingBinaries.pop(); + } else { + continue; + } + } + + // Look for an entitlements file in the bundle to include when signing + extraEntitlements = findEntitlementsFile(appBundleAbsolutePath + "/Contents/Resources/"); + + // All dependencies are signed, now sign this binary. + codesignFile(identity, binary); + signedBinaries.insert(binary); + pendingBinariesSet.remove(binary); + } + + LogNormal() << "Finished codesigning " << appBundlePath << "with identity" << identity; + + // Verify code signature + QProcess codesign; + codesign.start("codesign", QStringList() << "--deep" << "-v" << appBundlePath); + codesign.waitForFinished(-1); + QByteArray err = codesign.readAllStandardError(); + if (codesign.exitCode() > 0) { + LogError() << "codesign verification error:"; + LogError() << err; + } else if (!err.isEmpty()) { + LogDebug() << err; + } + + return signedBinaries; +} + +void codesign(const QString &identity, const QString &appBundlePath) { + codesignBundle(identity, appBundlePath, QList<QString>()); +} + +void createDiskImage(const QString &appBundlePath, const QString &filesystemType) +{ + QString appBaseName = appBundlePath; + appBaseName.chop(4); // remove ".app" from end + + QString dmgName = appBaseName + ".dmg"; + + QFile dmg(dmgName); + + if (dmg.exists() && alwaysOwerwriteEnabled) + dmg.remove(); + + if (dmg.exists()) { + LogNormal() << "Disk image already exists, skipping .dmg creation for" << dmg.fileName(); + } else { + LogNormal() << "Creating disk image (.dmg) for" << appBundlePath; + } + + LogNormal() << "Image will use" << filesystemType; + + // More dmg options can be found in the hdiutil man page. + QStringList options = QStringList() + << "create" << dmgName + << "-srcfolder" << appBundlePath + << "-format" << "UDZO" + << "-fs" << filesystemType + << "-volname" << appBaseName; + + QProcess hdutil; + hdutil.start("hdiutil", options); + hdutil.waitForFinished(-1); + if (hdutil.exitCode() != 0) { + LogError() << "Bundle creation error:" << hdutil.readAllStandardError(); + } +} + +void fixupFramework(const QString &frameworkName) +{ + // Expected framework name looks like "Foo.framework" + QStringList parts = frameworkName.split("."); + if (parts.count() < 2) { + LogError() << "fixupFramework: Unexpected framework name" << frameworkName; + return; + } + + // Assume framework binary path is Foo.framework/Foo + QString frameworkBinary = frameworkName + QStringLiteral("/") + parts[0]; + + // Xcode expects to find Foo.framework/Versions/A when code + // signing, while qmake typically generates numeric versions. + // Create symlink to the actual version in the framework. + linkFilePrintStatus("Current", frameworkName + "/Versions/A"); + + // Set up @rpath structure. + changeIdentification("@rpath/" + frameworkBinary, frameworkBinary); + addRPath("@loader_path/../../Contents/Frameworks/", frameworkBinary); +} diff --git a/src/tools/macdeployqt/shared/shared.h b/src/tools/macdeployqt/shared/shared.h new file mode 100644 index 0000000000..48ea24b119 --- /dev/null +++ b/src/tools/macdeployqt/shared/shared.h @@ -0,0 +1,141 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** 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 MAC_DEPLOMYMENT_SHARED_H +#define MAC_DEPLOMYMENT_SHARED_H + +#include <QString> +#include <QStringList> +#include <QDebug> +#include <QSet> +#include <QVersionNumber> + +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 bool runStripEnabled; + +class FrameworkInfo +{ +public: + bool isDylib; + QString frameworkDirectory; + QString frameworkName; + QString frameworkPath; + QString binaryDirectory; + QString binaryName; + QString binaryPath; + QString rpathUsed; + QString version; + QString installName; + QString deployedInstallName; + QString sourceFilePath; + QString frameworkDestinationDirectory; + QString binaryDestinationDirectory; + + bool isDebugLibrary() const + { + return binaryName.endsWith(QStringLiteral("_debug")); + } +}; + +class DylibInfo +{ +public: + QString binaryPath; + QVersionNumber currentVersion; + QVersionNumber compatibilityVersion; +}; + +class OtoolInfo +{ +public: + QString installName; + QString binaryPath; + QVersionNumber currentVersion; + QVersionNumber compatibilityVersion; + QList<DylibInfo> dependencies; +}; + +bool operator==(const FrameworkInfo &a, const FrameworkInfo &b); +QDebug operator<<(QDebug debug, const FrameworkInfo &info); + +class ApplicationBundleInfo +{ + public: + QString path; + QString binaryPath; + QStringList libraryPaths; +}; + +class DeploymentInfo +{ +public: + QString qtPath; + QString pluginPath; + QStringList deployedFrameworks; + QList<QString> rpathsUsed; + bool useLoaderPath; + bool isFramework; + bool isDebug; + + bool containsModule(const QString &module, const QString &libInFix) const; +}; + +inline QDebug operator<<(QDebug debug, const ApplicationBundleInfo &info); + +OtoolInfo findDependencyInfo(const QString &binaryPath); +FrameworkInfo parseOtoolLibraryLine(const QString &line, const QString &appBundlePath, const QList<QString> &rpaths, bool useDebugLibs); +QString findAppBinary(const QString &appBundlePath); +QList<FrameworkInfo> getQtFrameworks(const QString &path, const QString &appBundlePath, const QList<QString> &rpaths, bool useDebugLibs); +QList<FrameworkInfo> getQtFrameworks(const QStringList &otoolLines, const QString &appBundlePath, const QList<QString> &rpaths, bool useDebugLibs); +QString copyFramework(const FrameworkInfo &framework, const QString path); +DeploymentInfo deployQtFrameworks(const QString &appBundlePath, const QStringList &additionalExecutables, bool useDebugLibs); +DeploymentInfo deployQtFrameworks(QList<FrameworkInfo> frameworks,const QString &bundlePath, const QStringList &binaryPaths, bool useDebugLibs, bool useLoaderPath); +void createQtConf(const QString &appBundlePath); +void deployPlugins(const QString &appBundlePath, DeploymentInfo deploymentInfo, bool useDebugLibs); +bool deployQmlImports(const QString &appBundlePath, DeploymentInfo deploymentInfo, QStringList &qmlDirs, QStringList &qmlImportPaths); +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 &appBundlePath); +QStringList findAppFrameworkNames(const QString &appBundlePath); +QStringList findAppFrameworkPaths(const QString &appBundlePath); +void codesignFile(const QString &identity, const QString &filePath); +QSet<QString> codesignBundle(const QString &identity, + const QString &appBundlePath, + QList<QString> additionalBinariesContainingRpaths); +void codesign(const QString &identity, const QString &appBundlePath); +void createDiskImage(const QString &appBundlePath, const QString &filesystemType); +void fixupFramework(const QString &appBundlePath); + + +#endif diff --git a/src/tools/windeployqt/CMakeLists.txt b/src/tools/windeployqt/CMakeLists.txt new file mode 100644 index 0000000000..a63af56efb --- /dev/null +++ b/src/tools/windeployqt/CMakeLists.txt @@ -0,0 +1,32 @@ +##################################################################### +## windeployqt Tool: +##################################################################### + +qt_get_tool_target_name(target_name windeployqt) +qt_internal_add_tool(${target_name} + TOOLS_TARGET Core + USER_FACING + TARGET_DESCRIPTION "Qt Windows Deployment Tool" + SOURCES + elfreader.cpp elfreader.h + qmlutils.cpp qmlutils.h + utils.cpp utils.h + main.cpp + DEFINES + QT_NO_CAST_FROM_ASCII + QT_NO_CAST_TO_ASCII + QT_NO_FOREACH + LIBRARIES + Qt::CorePrivate +) +qt_internal_return_unless_building_tools() + +qt_internal_extend_target(${target_name} CONDITION WIN32 + PUBLIC_LIBRARIES + shlwapi +) + +qt_internal_extend_target(${target_name} CONDITION QT_FEATURE_relocatable + DEFINES + QT_RELOCATABLE +) diff --git a/src/tools/windeployqt/elfreader.cpp b/src/tools/windeployqt/elfreader.cpp new file mode 100644 index 0000000000..f375f5841d --- /dev/null +++ b/src/tools/windeployqt/elfreader.cpp @@ -0,0 +1,440 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** 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 "elfreader.h" + +#include <QDir> + +QT_BEGIN_NAMESPACE + +/* This is a copy of the ELF reader contained in Qt Creator (src/libs/utils), + * extended by the dependencies() function to read out the dependencies of a dynamic executable. */ + +quint16 getHalfWord(const unsigned char *&s, const ElfData &context) +{ + quint16 res; + if (context.endian == Elf_ELFDATA2MSB) + res = qFromBigEndian<quint16>(s); + else + res = qFromLittleEndian<quint16>(s); + s += 2; + return res; +} + +quint32 getWord(const unsigned char *&s, const ElfData &context) +{ + quint32 res; + if (context.endian == Elf_ELFDATA2MSB) + res = qFromBigEndian<quint32>(s); + else + res = qFromLittleEndian<quint32>(s); + s += 4; + return res; +} + +quint64 getAddress(const unsigned char *&s, const ElfData &context) +{ + quint64 res; + if (context.elfclass == Elf_ELFCLASS32) { + if (context.endian == Elf_ELFDATA2MSB) + res = qFromBigEndian<quint32>(s); + else + res = qFromLittleEndian<quint32>(s); + s += 4; + } else { + if (context.endian == Elf_ELFDATA2MSB) + res = qFromBigEndian<quint64>(s); + else + res = qFromLittleEndian<quint64>(s); + s += 8; + } + return res; +} + +quint64 getOffset(const unsigned char *&s, const ElfData &context) +{ + return getAddress(s, context); +} + +static void parseSectionHeader(const uchar *s, ElfSectionHeader *sh, const ElfData &context) +{ + sh->index = getWord(s, context); + sh->type = getWord(s, context); + sh->flags = quint32(getOffset(s, context)); + sh->addr = getAddress(s, context); + sh->offset = getOffset(s, context); + sh->size = getOffset(s, context); +} + +static void parseProgramHeader(const uchar *s, ElfProgramHeader *sh, const ElfData &context) +{ + sh->type = getWord(s, context); + sh->offset = getOffset(s, context); + /* p_vaddr = */ getAddress(s, context); + /* p_paddr = */ getAddress(s, context); + sh->filesz = getWord(s, context); + sh->memsz = getWord(s, context); +} + +class ElfMapper +{ +public: + ElfMapper(const ElfReader *reader) : file(reader->m_binary) {} + + bool map() + { + if (!file.open(QIODevice::ReadOnly)) + return false; + + fdlen = quint64(file.size()); + ustart = file.map(0, qint64(fdlen)); + if (ustart == 0) { + // Try reading the data into memory instead. + raw = file.readAll(); + start = raw.constData(); + fdlen = quint64(raw.size()); + } + return true; + } + +public: + QFile file; + QByteArray raw; + union { const char *start; const uchar *ustart; }; + quint64 fdlen; +}; + +ElfReader::ElfReader(const QString &binary) + : m_binary(binary) +{ +} + +ElfData ElfReader::readHeaders() +{ + readIt(); + return m_elfData; +} + +static inline QString msgInvalidElfObject(const QString &binary, const QString &why) +{ + return QStringLiteral("'%1' is an invalid ELF object (%2)") + .arg(QDir::toNativeSeparators(binary), why); +} + +ElfReader::Result ElfReader::readIt() +{ + if (!m_elfData.sectionHeaders.isEmpty()) + return Ok; + if (!m_elfData.programHeaders.isEmpty()) + return Ok; + + ElfMapper mapper(this); + if (!mapper.map()) + return Corrupt; + + const quint64 fdlen = mapper.fdlen; + + if (fdlen < 64) { + m_errorString = QStringLiteral("'%1' is not an ELF object (file too small)").arg(QDir::toNativeSeparators(m_binary)); + return NotElf; + } + + if (strncmp(mapper.start, "\177ELF", 4) != 0) { + m_errorString = QStringLiteral("'%1' is not an ELF object").arg(QDir::toNativeSeparators(m_binary)); + return NotElf; + } + + // 32 or 64 bit + m_elfData.elfclass = ElfClass(mapper.start[4]); + const bool is64Bit = m_elfData.elfclass == Elf_ELFCLASS64; + if (m_elfData.elfclass != Elf_ELFCLASS32 && m_elfData.elfclass != Elf_ELFCLASS64) { + m_errorString = msgInvalidElfObject(m_binary, QStringLiteral("odd cpu architecture")); + return Corrupt; + } + + // int bits = (data[4] << 5); + // If you remove this check to read ELF objects of a different arch, + // please make sure you modify the typedefs + // to match the _plugin_ architecture. + // if ((sizeof(void*) == 4 && bits != 32) + // || (sizeof(void*) == 8 && bits != 64)) { + // if (errorString) + // *errorString = QLibrary::QStringLiteral("'%1' is an invalid ELF object (%2)") + // .arg(m_binary).arg(QLatin1String("wrong cpu architecture")); + // return Corrupt; + // } + + // Read Endianhness. + m_elfData.endian = ElfEndian(mapper.ustart[5]); + if (m_elfData.endian != Elf_ELFDATA2LSB && m_elfData.endian != Elf_ELFDATA2MSB) { + m_errorString = msgInvalidElfObject(m_binary, QStringLiteral("odd endianness")); + return Corrupt; + } + + const uchar *data = mapper.ustart + 16; // e_ident + m_elfData.elftype = ElfType(getHalfWord(data, m_elfData)); + m_elfData.elfmachine = ElfMachine(getHalfWord(data, m_elfData)); + /* e_version = */ getWord(data, m_elfData); + m_elfData.entryPoint = getAddress(data, m_elfData); + + quint64 e_phoff = getOffset(data, m_elfData); + quint64 e_shoff = getOffset(data, m_elfData); + /* e_flags = */ getWord(data, m_elfData); + + quint32 e_shsize = getHalfWord(data, m_elfData); + + if (e_shsize > fdlen) { + m_errorString = msgInvalidElfObject(m_binary, QStringLiteral("unexpected e_shsize")); + return Corrupt; + } + + quint32 e_phentsize = getHalfWord(data, m_elfData); + if (e_phentsize != (is64Bit ? 56 : 32)) { + m_errorString = msgInvalidElfObject(m_binary, QStringLiteral("invalid structure")); + return ElfReader::Corrupt; + } + quint32 e_phnum = getHalfWord(data, m_elfData); + + quint32 e_shentsize = getHalfWord(data, m_elfData); + + if (e_shentsize % 4) { + m_errorString = msgInvalidElfObject(m_binary, QStringLiteral("unexpected e_shentsize")); + return Corrupt; + } + + quint32 e_shnum = getHalfWord(data, m_elfData); + quint32 e_shtrndx = getHalfWord(data, m_elfData); + if (data != mapper.ustart + (is64Bit ? 64 : 52)) { + m_errorString = msgInvalidElfObject(m_binary, QStringLiteral("unexpected e_phentsize")); + return ElfReader::Corrupt; + } + + if (quint64(e_shnum) * e_shentsize > fdlen) { + const QString reason = QStringLiteral("announced %1 sections, each %2 bytes, exceed file size").arg(e_shnum).arg(e_shentsize); + m_errorString = msgInvalidElfObject(m_binary, reason); + return Corrupt; + } + + quint64 soff = e_shoff + e_shentsize * e_shtrndx; + +// if ((soff + e_shentsize) > fdlen || soff % 4 || soff == 0) { +// m_errorString = QLibrary::QStringLiteral("'%1' is an invalid ELF object (%2)") +// .arg(m_binary) +// .arg(QLatin1String("shstrtab section header seems to be at %1")) +// .arg(QString::number(soff, 16)); +// return Corrupt; +// } + + if (e_shoff) { + ElfSectionHeader strtab; + parseSectionHeader(mapper.ustart + soff, &strtab, m_elfData); + const quint64 stringTableFileOffset = strtab.offset; + if (quint32(stringTableFileOffset + e_shentsize) >= fdlen + || stringTableFileOffset == 0) { + const QString reason = QStringLiteral("string table seems to be at 0x%1").arg(soff, 0, 16); + m_errorString = msgInvalidElfObject(m_binary, reason); + return Corrupt; + } + + for (quint32 i = 0; i < e_shnum; ++i) { + const uchar *s = mapper.ustart + e_shoff + i * e_shentsize; + ElfSectionHeader sh; + parseSectionHeader(s, &sh, m_elfData); + + if (stringTableFileOffset + sh.index > fdlen) { + const QString reason = QStringLiteral("section name %1 of %2 behind end of file") + .arg(i).arg(e_shnum); + m_errorString = msgInvalidElfObject(m_binary, reason); + return Corrupt; + } + + sh.name = mapper.start + stringTableFileOffset + sh.index; + if (sh.name == ".gdb_index") { + m_elfData.symbolsType = FastSymbols; + } else if (sh.name == ".debug_info") { + m_elfData.symbolsType = PlainSymbols; + } else if (sh.name == ".gnu_debuglink") { + m_elfData.debugLink = QByteArray(mapper.start + sh.offset); + m_elfData.symbolsType = LinkedSymbols; + } else if (sh.name == ".note.gnu.build-id") { + m_elfData.symbolsType = BuildIdSymbols; + if (sh.size > 16) + m_elfData.buildId = QByteArray(mapper.start + sh.offset + 16, + int(sh.size) - 16).toHex(); + } + m_elfData.sectionHeaders.append(sh); + } + } + + if (e_phoff) { + for (quint32 i = 0; i < e_phnum; ++i) { + const uchar *s = mapper.ustart + e_phoff + i * e_phentsize; + ElfProgramHeader ph; + parseProgramHeader(s, &ph, m_elfData); + m_elfData.programHeaders.append(ph); + } + } + return Ok; +} + +QByteArray ElfReader::readSection(const QByteArray &name) +{ + readIt(); + int i = m_elfData.indexOf(name); + if (i == -1) + return QByteArray(); + + ElfMapper mapper(this); + if (!mapper.map()) + return QByteArray(); + + const ElfSectionHeader §ion = m_elfData.sectionHeaders.at(i); + return QByteArray(mapper.start + section.offset, int(section.size)); +} + +static QByteArray cutout(const char *s) +{ + QByteArray res(s, 80); + const int pos = res.indexOf('\0'); + if (pos != -1) + res.resize(pos - 1); + return res; +} + +QByteArray ElfReader::readCoreName(bool *isCore) +{ + *isCore = false; + + readIt(); + + ElfMapper mapper(this); + if (!mapper.map()) + return QByteArray(); + + if (m_elfData.elftype != Elf_ET_CORE) + return QByteArray(); + + *isCore = true; + + for (int i = 0, n = m_elfData.sectionHeaders.size(); i != n; ++i) + if (m_elfData.sectionHeaders.at(i).type == Elf_SHT_NOTE) { + const ElfSectionHeader &header = m_elfData.sectionHeaders.at(i); + return cutout(mapper.start + header.offset + 0x40); + } + + for (int i = 0, n = m_elfData.programHeaders.size(); i != n; ++i) + if (m_elfData.programHeaders.at(i).type == Elf_PT_NOTE) { + const ElfProgramHeader &header = m_elfData.programHeaders.at(i); + return cutout(mapper.start + header.offset + 0xec); + } + + return QByteArray(); +} + +int ElfData::indexOf(const QByteArray &name) const +{ + for (int i = 0, n = sectionHeaders.size(); i != n; ++i) + if (sectionHeaders.at(i).name == name) + return i; + return -1; +} + +/* Helpers for reading out the .dynamic section containing the dependencies. + * The ".dynamic" section is an array of + * typedef struct { + * Elf32_Sword d_tag; + * union { + * Elf32_Word d_val; + * dElf32_Addr d_ptr; + * } d_un; + * } Elf32_Dyn + * with entries where a tag DT_NEEDED indicates that m_val is an offset into + * the string table ".dynstr". The documentation states that entries with the + * tag DT_STRTAB contain an offset for the string table to be used, but that + * has been found not to contain valid entries. */ + +enum DynamicSectionTags { + DT_NULL = 0, + DT_NEEDED = 1, + DT_STRTAB = 5, + DT_SONAME = 14, + DT_RPATH = 15 +}; + +QList<QByteArray> ElfReader::dependencies() +{ + QList<QByteArray> result; + + ElfMapper mapper(this); + if (!mapper.map()) { + m_errorString = QStringLiteral("Mapper failure"); + return result; + } + quint64 dynStrOffset = 0; + quint64 dynamicOffset = 0; + quint64 dynamicSize = 0; + + const QList<ElfSectionHeader> &headers = readHeaders().sectionHeaders; + for (const ElfSectionHeader &eh : headers) { + if (eh.name == QByteArrayLiteral(".dynstr")) { + dynStrOffset = eh.offset; + } else if (eh.name == QByteArrayLiteral(".dynamic")) { + dynamicOffset = eh.offset; + dynamicSize = eh.size; + } + if (dynStrOffset && dynamicOffset) + break; + } + + if (!dynStrOffset || !dynamicOffset) { + m_errorString = QStringLiteral("Not a dynamically linked executable."); + return result; + } + + const unsigned char *dynamicData = mapper.ustart + dynamicOffset; + const unsigned char *dynamicDataEnd = dynamicData + dynamicSize; + while (dynamicData < dynamicDataEnd) { + const quint32 tag = getWord(dynamicData, m_elfData); + if (tag == DT_NULL) + break; + if (m_elfData.elfclass == Elf_ELFCLASS64) + dynamicData += sizeof(quint32); // padding to d_val/d_ptr. + if (tag == DT_NEEDED) { + const quint32 offset = getWord(dynamicData, m_elfData); + if (m_elfData.elfclass == Elf_ELFCLASS64) + dynamicData += sizeof(quint32); // past d_ptr. + const char *name = mapper.start + dynStrOffset + offset; + result.push_back(name); + } else { + dynamicData += m_elfData.elfclass == Elf_ELFCLASS64 ? 8 : 4; + } + } + return result; +} + +QT_END_NAMESPACE diff --git a/src/tools/windeployqt/elfreader.h b/src/tools/windeployqt/elfreader.h new file mode 100644 index 0000000000..d84f5a0602 --- /dev/null +++ b/src/tools/windeployqt/elfreader.h @@ -0,0 +1,176 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** 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 ELFREADER_H +#define ELFREADER_H + +#include <QtCore/QList> +#include <QtCore/QString> +#include <QtCore/QtEndian> + +QT_BEGIN_NAMESPACE + +enum ElfProgramHeaderType +{ + Elf_PT_NULL = 0, + Elf_PT_LOAD = 1, + Elf_PT_DYNAMIC = 2, + Elf_PT_INTERP = 3, + Elf_PT_NOTE = 4, + Elf_PT_SHLIB = 5, + Elf_PT_PHDR = 6, + Elf_PT_TLS = 7, + Elf_PT_NUM = 8 +}; + +enum ElfSectionHeaderType +{ + Elf_SHT_NULL = 0, + Elf_SHT_PROGBITS = 1, + Elf_SHT_SYMTAB = 2, + Elf_SHT_STRTAB = 3, + Elf_SHT_RELA = 4, + Elf_SHT_HASH = 5, + Elf_SHT_DYNAMIC = 6, + Elf_SHT_NOTE = 7, + Elf_SHT_NOBITS = 8, + Elf_SHT_REL = 9, + Elf_SHT_SHLIB = 10, + Elf_SHT_DYNSYM = 11, + Elf_SHT_INIT_ARRAY = 14, + Elf_SHT_FINI_ARRAY = 15, + Elf_SHT_PREINIT_ARRAY = 16, + Elf_SHT_GROUP = 17, + Elf_SHT_SYMTAB_SHNDX = 18 +}; + +enum ElfEndian +{ + Elf_ELFDATANONE = 0, + Elf_ELFDATA2LSB = 1, + Elf_ELFDATA2MSB = 2, + Elf_ELFDATANUM = 3 +}; + +enum ElfClass +{ + Elf_ELFCLASS32 = 1, + Elf_ELFCLASS64 = 2 +}; + +enum ElfType +{ + Elf_ET_NONE = 0, + Elf_ET_REL = 1, + Elf_ET_EXEC = 2, + Elf_ET_DYN = 3, + Elf_ET_CORE = 4 +}; + +enum ElfMachine +{ + Elf_EM_386 = 3, + Elf_EM_ARM = 40, + Elf_EM_X86_64 = 62 +}; + +enum DebugSymbolsType +{ + UnknownSymbols = 0, // Unknown. + NoSymbols = 1, // No usable symbols. + LinkedSymbols = 2, // Link to symols available. + BuildIdSymbols = 4, // BuildId available. + PlainSymbols = 8, // Ordinary symbols available. + FastSymbols = 16 // Dwarf index available. +}; + +class ElfSectionHeader +{ +public: + QByteArray name; + quint32 index; + quint32 type; + quint32 flags; + quint64 offset; + quint64 size; + quint64 addr; +}; + +class ElfProgramHeader +{ +public: + quint32 name; + quint32 type; + quint64 offset; + quint64 filesz; + quint64 memsz; +}; + +class ElfData +{ +public: + ElfData() : symbolsType(UnknownSymbols) {} + int indexOf(const QByteArray &name) const; + +public: + ElfEndian endian; + ElfType elftype; + ElfMachine elfmachine; + ElfClass elfclass; + quint64 entryPoint; + QByteArray debugLink; + QByteArray buildId; + DebugSymbolsType symbolsType; + QList<ElfSectionHeader> sectionHeaders; + QList<ElfProgramHeader> programHeaders; +}; + +class ElfReader +{ +public: + explicit ElfReader(const QString &binary); + enum Result { Ok, NotElf, Corrupt }; + + ElfData readHeaders(); + QByteArray readSection(const QByteArray §ionName); + QString errorString() const { return m_errorString; } + QByteArray readCoreName(bool *isCore); + QList<QByteArray> dependencies(); + +private: + friend class ElfMapper; + Result readIt(); + + QString m_binary; + QString m_errorString; + ElfData m_elfData; +}; + +QT_END_NAMESPACE + +#endif // ELFREADER_H diff --git a/src/tools/windeployqt/main.cpp b/src/tools/windeployqt/main.cpp new file mode 100644 index 0000000000..27b57ea9ad --- /dev/null +++ b/src/tools/windeployqt/main.cpp @@ -0,0 +1,1723 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** 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 "utils.h" +#include "qmlutils.h" + +#include <QtCore/QCommandLineOption> +#include <QtCore/QCommandLineParser> +#include <QtCore/QDir> +#include <QtCore/QFileInfo> +#include <QtCore/QCoreApplication> +#include <QtCore/QJsonDocument> +#include <QtCore/QJsonObject> +#include <QtCore/QJsonArray> +#include <QtCore/QList> +#include <QtCore/QOperatingSystemVersion> +#include <QtCore/QSharedPointer> + +#ifdef Q_OS_WIN +#include <QtCore/qt_windows.h> +#else +#define IMAGE_FILE_MACHINE_ARM64 0xaa64 +#endif + +#include <algorithm> +#include <iostream> +#include <iterator> +#include <cstdio> + +QT_BEGIN_NAMESPACE + +enum QtModule +#if defined(Q_COMPILER_CLASS_ENUM) || defined(Q_CC_MSVC) + : quint64 +#endif +{ + QtBluetoothModule = 0x0000000000000001, + QtConcurrentModule = 0x0000000000000002, + QtCoreModule = 0x0000000000000004, + QtDeclarativeModule = 0x0000000000000008, + QtDesignerComponents = 0x0000000000000010, + QtDesignerModule = 0x0000000000000020, + QtGuiModule = 0x0000000000000040, + QtHelpModule = 0x0000000000000080, + QtMultimediaModule = 0x0000000000000100, + QtMultimediaWidgetsModule = 0x0000000000000200, + QtMultimediaQuickModule = 0x0000000000000400, + QtNetworkModule = 0x0000000000000800, + QtNfcModule = 0x0000000000001000, + QtOpenGLModule = 0x0000000000002000, + QtOpenGLWidgetsModule = 0x0000000000004000, + QtPositioningModule = 0x0000000000008000, + QtPrintSupportModule = 0x0000000000010000, + QtQmlModule = 0x0000000000020000, + QtQuickModule = 0x0000000000040000, + QtQuickParticlesModule = 0x0000000000080000, + QtScriptModule = 0x0000000000100000, + QtScriptToolsModule = 0x0000000000200000, + QtSensorsModule = 0x0000000000400000, + QtSerialPortModule = 0x0000000000800000, + QtSqlModule = 0x0000000001000000, + QtSvgModule = 0x0000000002000000, + QtSvgWidgetsModule = 0x0000000004000000, + QtTestModule = 0x0000000008000000, + QtWidgetsModule = 0x0000000010000000, + QtWinExtrasModule = 0x0000000020000000, + QtXmlModule = 0x0000000040000000, + QtQuickWidgetsModule = 0x0000000100000000, + QtWebSocketsModule = 0x0000000200000000, + QtWebEngineCoreModule = 0x0000000800000000, + QtWebEngineModule = 0x0000001000000000, + QtWebEngineWidgetsModule = 0x0000002000000000, + QtQmlToolingModule = 0x0000004000000000, + Qt3DCoreModule = 0x0000008000000000, + Qt3DRendererModule = 0x0000010000000000, + Qt3DQuickModule = 0x0000020000000000, + Qt3DQuickRendererModule = 0x0000040000000000, + Qt3DInputModule = 0x0000080000000000, + QtLocationModule = 0x0000100000000000, + QtWebChannelModule = 0x0000200000000000, + QtTextToSpeechModule = 0x0000400000000000, + QtSerialBusModule = 0x0000800000000000, + QtGamePadModule = 0x0001000000000000, + Qt3DAnimationModule = 0x0002000000000000, + QtWebViewModule = 0x0004000000000000, + Qt3DExtrasModule = 0x0008000000000000, + QtShaderToolsModule = 0x0010000000000000 +}; + +struct QtModuleEntry { + quint64 module; + const char *option; + const char *libraryName; + const char *translation; +}; + +static QtModuleEntry qtModuleEntries[] = { + { QtBluetoothModule, "bluetooth", "Qt6Bluetooth", nullptr }, + { QtConcurrentModule, "concurrent", "Qt6Concurrent", "qtbase" }, + { QtCoreModule, "core", "Qt6Core", "qtbase" }, + { QtDeclarativeModule, "declarative", "Qt6Declarative", "qtquick1" }, + { QtDesignerModule, "designer", "Qt6Designer", nullptr }, + { QtDesignerComponents, "designercomponents", "Qt6DesignerComponents", nullptr }, + { QtGamePadModule, "gamepad", "Qt6Gamepad", nullptr }, + { QtGuiModule, "gui", "Qt6Gui", "qtbase" }, + { QtHelpModule, "qthelp", "Qt6Help", "qt_help" }, + { QtMultimediaModule, "multimedia", "Qt6Multimedia", "qtmultimedia" }, + { QtMultimediaWidgetsModule, "multimediawidgets", "Qt6MultimediaWidgets", "qtmultimedia" }, + { QtMultimediaQuickModule, "multimediaquick", "Qt6MultimediaQuick_p", "qtmultimedia" }, + { QtNetworkModule, "network", "Qt6Network", "qtbase" }, + { QtNfcModule, "nfc", "Qt6Nfc", nullptr }, + { QtOpenGLModule, "opengl", "Qt6OpenGL", nullptr }, + { QtOpenGLWidgetsModule, "openglwidgets", "Qt6OpenGLWidgets", nullptr }, + { QtPositioningModule, "positioning", "Qt6Positioning", nullptr }, + { QtPrintSupportModule, "printsupport", "Qt6PrintSupport", nullptr }, + { QtQmlModule, "qml", "Qt6Qml", "qtdeclarative" }, + { QtQmlToolingModule, "qmltooling", "qmltooling", nullptr }, + { QtQuickModule, "quick", "Qt6Quick", "qtdeclarative" }, + { QtQuickParticlesModule, "quickparticles", "Qt6QuickParticles", nullptr }, + { QtQuickWidgetsModule, "quickwidgets", "Qt6QuickWidgets", nullptr }, + { QtScriptModule, "script", "Qt6Script", "qtscript" }, + { QtScriptToolsModule, "scripttools", "Qt6ScriptTools", "qtscript" }, + { QtSensorsModule, "sensors", "Qt6Sensors", nullptr }, + { QtSerialPortModule, "serialport", "Qt6SerialPort", "qtserialport" }, + { QtSqlModule, "sql", "Qt6Sql", "qtbase" }, + { QtSvgModule, "svg", "Qt6Svg", nullptr }, + { QtSvgWidgetsModule, "svgwidgets", "Qt6SvgWidgets", nullptr }, + { QtTestModule, "test", "Qt6Test", "qtbase" }, + { QtWebSocketsModule, "websockets", "Qt6WebSockets", nullptr }, + { QtWidgetsModule, "widgets", "Qt6Widgets", "qtbase" }, + { QtWinExtrasModule, "winextras", "Qt6WinExtras", nullptr }, + { QtXmlModule, "xml", "Qt6Xml", "qtbase" }, + { QtWebEngineCoreModule, "webenginecore", "Qt6WebEngineCore", nullptr }, + { QtWebEngineModule, "webengine", "Qt6WebEngine", "qtwebengine" }, + { QtWebEngineWidgetsModule, "webenginewidgets", "Qt6WebEngineWidgets", nullptr }, + { Qt3DCoreModule, "3dcore", "Qt63DCore", nullptr }, + { Qt3DRendererModule, "3drenderer", "Qt63DRender", nullptr }, + { Qt3DQuickModule, "3dquick", "Qt63DQuick", nullptr }, + { Qt3DQuickRendererModule, "3dquickrenderer", "Qt63DQuickRender", nullptr }, + { Qt3DInputModule, "3dinput", "Qt63DInput", nullptr }, + { Qt3DAnimationModule, "3danimation", "Qt63DAnimation", nullptr }, + { Qt3DExtrasModule, "3dextras", "Qt63DExtras", nullptr }, + { QtLocationModule, "geoservices", "Qt6Location", nullptr }, + { QtWebChannelModule, "webchannel", "Qt6WebChannel", nullptr }, + { QtTextToSpeechModule, "texttospeech", "Qt6TextToSpeech", nullptr }, + { QtSerialBusModule, "serialbus", "Qt6SerialBus", nullptr }, + { QtWebViewModule, "webview", "Qt6WebView", nullptr }, + { QtShaderToolsModule, "shadertools", "Qt6ShaderTools", nullptr } +}; + +enum QtPlugin { + QtVirtualKeyboardPlugin = 0x1 +}; + +static const char webEngineProcessC[] = "QtWebEngineProcess"; + +static inline QString webProcessBinary(const char *binaryName, Platform p) +{ + const QString webProcess = QLatin1String(binaryName); + return (p & WindowsBased) ? webProcess + QStringLiteral(".exe") : webProcess; +} + +static QByteArray formatQtModules(quint64 mask, bool option = false) +{ + QByteArray result; + for (const auto &qtModule : qtModuleEntries) { + if (mask & qtModule.module) { + if (!result.isEmpty()) + result.append(' '); + result.append(option ? qtModule.option : qtModule.libraryName); + } + } + return result; +} + +static Platform platformFromMkSpec(const QString &xSpec) +{ + if (xSpec == QLatin1String("linux-g++")) + return Unix; + if (xSpec.startsWith(QLatin1String("win32-"))) { + if (xSpec.contains(QLatin1String("clang-g++"))) + return WindowsDesktopClangMinGW; + if (xSpec.contains(QLatin1String("clang-msvc++"))) + return WindowsDesktopClangMsvc; + return xSpec.contains(QLatin1String("g++")) ? WindowsDesktopMinGW : WindowsDesktopMsvc; + } + return UnknownPlatform; +} + +// Helpers for exclusive options, "-foo", "--no-foo" +enum ExlusiveOptionValue { + OptionAuto, + OptionEnabled, + OptionDisabled +}; + +static ExlusiveOptionValue parseExclusiveOptions(const QCommandLineParser *parser, + const QCommandLineOption &enableOption, + const QCommandLineOption &disableOption) +{ + const bool enabled = parser->isSet(enableOption); + const bool disabled = parser->isSet(disableOption); + if (enabled) { + if (disabled) { + std::wcerr << "Warning: both -" << enableOption.names().first() + << " and -" << disableOption.names().first() << " were specified, defaulting to -" + << enableOption.names().first() << ".\n"; + } + return OptionEnabled; + } + return disabled ? OptionDisabled : OptionAuto; +} + +struct Options { + enum DebugDetection { + DebugDetectionAuto, + DebugDetectionForceDebug, + DebugDetectionForceRelease + }; + + bool plugins = true; + bool libraries = true; + bool quickImports = true; + bool translations = true; + bool systemD3dCompiler = true; + bool compilerRunTime = false; + unsigned disabledPlugins = 0; + bool softwareRasterizer = true; + Platform platform = WindowsDesktopMsvc; + quint64 additionalLibraries = 0; + quint64 disabledLibraries = 0; + unsigned updateFileFlags = 0; + QStringList qmlDirectories; // Project's QML files. + QStringList qmlImportPaths; // Custom QML module locations. + QString directory; + QString qtpathsBinary; + QString translationsDirectory; // Translations target directory + QStringList languages; + QString libraryDirectory; + QString pluginDirectory; + QStringList binaries; + JsonOutput *json = nullptr; + ListOption list = ListNone; + DebugDetection debugDetection = DebugDetectionAuto; + bool deployPdb = false; + bool dryRun = false; + bool patchQt = true; + bool ignoreLibraryErrors = false; +}; + +// Return binary to be deployed from folder, ignore pre-existing web engine process. +static inline QString findBinary(const QString &directory, Platform platform) +{ + const QStringList nameFilters = (platform & WindowsBased) ? + QStringList(QStringLiteral("*.exe")) : QStringList(); + const QFileInfoList &binaries = + QDir(QDir::cleanPath(directory)).entryInfoList(nameFilters, QDir::Files | QDir::Executable); + for (const QFileInfo &binaryFi : binaries) { + const QString binary = binaryFi.fileName(); + if (!binary.contains(QLatin1String(webEngineProcessC), Qt::CaseInsensitive)) { + return binaryFi.absoluteFilePath(); + } + } + return QString(); +} + +static QString msgFileDoesNotExist(const QString & file) +{ + return QLatin1Char('"') + QDir::toNativeSeparators(file) + + QStringLiteral("\" does not exist."); +} + +enum CommandLineParseFlag { + CommandLineParseError = 0x1, + CommandLineParseHelpRequested = 0x2 +}; + +static inline int parseArguments(const QStringList &arguments, QCommandLineParser *parser, + Options *options, QString *errorMessage) +{ + using CommandLineOptionPtr = QSharedPointer<QCommandLineOption>; + using OptionPtrVector = QList<CommandLineOptionPtr>; + + parser->setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions); + parser->setApplicationDescription(QStringLiteral("Qt Deploy Tool ") + QLatin1String(QT_VERSION_STR) + + QLatin1String("\n\nThe simplest way to use windeployqt is to add the bin directory of your Qt\n" + "installation (e.g. <QT_DIR\\bin>) to the PATH variable and then run:\n windeployqt <path-to-app-binary>\n\n" + "If your application uses Qt Quick, run:\n windeployqt --qmldir <path-to-app-qml-files> <path-to-app-binary>")); + const QCommandLineOption helpOption = parser->addHelpOption(); + parser->addVersionOption(); + + QCommandLineOption dirOption(QStringLiteral("dir"), + QStringLiteral("Use directory instead of binary directory."), + QStringLiteral("directory")); + parser->addOption(dirOption); + + QCommandLineOption qmakeOption(QStringLiteral("qmake"), + QStringLiteral("Use specified qmake instead of qmake from PATH. " + "Deprecated, use qtpaths instead."), + QStringLiteral("path")); + parser->addOption(qmakeOption); + + QCommandLineOption qtpathsOption( + QStringLiteral("qtpaths"), + QStringLiteral("Use specified qtpaths.exe instead of qtpaths.exe from PATH."), + QStringLiteral("path")); + parser->addOption(qtpathsOption); + + QCommandLineOption libDirOption(QStringLiteral("libdir"), + QStringLiteral("Copy libraries to path."), + QStringLiteral("path")); + parser->addOption(libDirOption); + + QCommandLineOption pluginDirOption(QStringLiteral("plugindir"), + QStringLiteral("Copy plugins to path."), + QStringLiteral("path")); + parser->addOption(pluginDirOption); + + QCommandLineOption debugOption(QStringLiteral("debug"), + QStringLiteral("Assume debug binaries.")); + parser->addOption(debugOption); + QCommandLineOption releaseOption(QStringLiteral("release"), + QStringLiteral("Assume release binaries.")); + parser->addOption(releaseOption); + QCommandLineOption releaseWithDebugInfoOption(QStringLiteral("release-with-debug-info"), + QStringLiteral("Assume release binaries with debug information.")); + releaseWithDebugInfoOption.setFlags(QCommandLineOption::HiddenFromHelp); // Deprecated by improved debug detection. + parser->addOption(releaseWithDebugInfoOption); + + QCommandLineOption deployPdbOption(QStringLiteral("pdb"), + QStringLiteral("Deploy .pdb files (MSVC).")); + parser->addOption(deployPdbOption); + + QCommandLineOption forceOption(QStringLiteral("force"), + QStringLiteral("Force updating files.")); + parser->addOption(forceOption); + + QCommandLineOption dryRunOption(QStringLiteral("dry-run"), + QStringLiteral("Simulation mode. Behave normally, but do not copy/update any files.")); + parser->addOption(dryRunOption); + + QCommandLineOption noPatchQtOption(QStringLiteral("no-patchqt"), + QStringLiteral("Do not patch the Qt6Core library.")); + parser->addOption(noPatchQtOption); + + QCommandLineOption ignoreErrorOption(QStringLiteral("ignore-library-errors"), + QStringLiteral("Ignore errors when libraries cannot be found.")); + parser->addOption(ignoreErrorOption); + + QCommandLineOption noPluginsOption(QStringLiteral("no-plugins"), + QStringLiteral("Skip plugin deployment.")); + parser->addOption(noPluginsOption); + + QCommandLineOption noLibraryOption(QStringLiteral("no-libraries"), + QStringLiteral("Skip library deployment.")); + parser->addOption(noLibraryOption); + + QCommandLineOption qmlDirOption(QStringLiteral("qmldir"), + QStringLiteral("Scan for QML-imports starting from directory."), + QStringLiteral("directory")); + parser->addOption(qmlDirOption); + + QCommandLineOption qmlImportOption(QStringLiteral("qmlimport"), + QStringLiteral("Add the given path to the QML module search locations."), + QStringLiteral("directory")); + parser->addOption(qmlImportOption); + + QCommandLineOption noQuickImportOption(QStringLiteral("no-quick-import"), + QStringLiteral("Skip deployment of Qt Quick imports.")); + parser->addOption(noQuickImportOption); + + + QCommandLineOption translationOption(QStringLiteral("translations"), + QStringLiteral("A comma-separated list of languages to deploy (de,fi)."), + QStringLiteral("languages")); + parser->addOption(translationOption); + + QCommandLineOption noTranslationOption(QStringLiteral("no-translations"), + QStringLiteral("Skip deployment of translations.")); + parser->addOption(noTranslationOption); + + QCommandLineOption noSystemD3DCompilerOption(QStringLiteral("no-system-d3d-compiler"), + QStringLiteral("Skip deployment of the system D3D compiler.")); + parser->addOption(noSystemD3DCompilerOption); + + + QCommandLineOption compilerRunTimeOption(QStringLiteral("compiler-runtime"), + QStringLiteral("Deploy compiler runtime (Desktop only).")); + parser->addOption(compilerRunTimeOption); + + QCommandLineOption noVirtualKeyboardOption(QStringLiteral("no-virtualkeyboard"), + QStringLiteral("Disable deployment of the Virtual Keyboard.")); + parser->addOption(noVirtualKeyboardOption); + + QCommandLineOption noCompilerRunTimeOption(QStringLiteral("no-compiler-runtime"), + QStringLiteral("Do not deploy compiler runtime (Desktop only).")); + parser->addOption(noCompilerRunTimeOption); + + QCommandLineOption jsonOption(QStringLiteral("json"), + QStringLiteral("Print to stdout in JSON format.")); + parser->addOption(jsonOption); + + QCommandLineOption suppressSoftwareRasterizerOption(QStringLiteral("no-opengl-sw"), + QStringLiteral("Do not deploy the software rasterizer library.")); + parser->addOption(suppressSoftwareRasterizerOption); + + QCommandLineOption listOption(QStringLiteral("list"), + QLatin1String("Print only the names of the files copied.\n" + "Available options:\n" + " source: absolute path of the source files\n" + " target: absolute path of the target files\n" + " relative: paths of the target files, relative\n" + " to the target directory\n" + " mapping: outputs the source and the relative\n" + " target, suitable for use within an\n" + " Appx mapping file"), + QStringLiteral("option")); + parser->addOption(listOption); + + QCommandLineOption verboseOption(QStringLiteral("verbose"), + QStringLiteral("Verbose level (0-2)."), + QStringLiteral("level")); + parser->addOption(verboseOption); + + parser->addPositionalArgument(QStringLiteral("[files]"), + QStringLiteral("Binaries or directory containing the binary.")); + + OptionPtrVector enabledModuleOptions; + OptionPtrVector disabledModuleOptions; + const int qtModulesCount = int(sizeof(qtModuleEntries) / sizeof(QtModuleEntry)); + enabledModuleOptions.reserve(qtModulesCount); + disabledModuleOptions.reserve(qtModulesCount); + for (int i = 0; i < qtModulesCount; ++i) { + const QString option = QLatin1String(qtModuleEntries[i].option); + const QString name = QLatin1String(qtModuleEntries[i].libraryName); + const QString enabledDescription = QStringLiteral("Add ") + name + QStringLiteral(" module."); + CommandLineOptionPtr enabledOption(new QCommandLineOption(option, enabledDescription)); + parser->addOption(*enabledOption.data()); + enabledModuleOptions.append(enabledOption); + const QString disabledDescription = QStringLiteral("Remove ") + name + QStringLiteral(" module."); + CommandLineOptionPtr disabledOption(new QCommandLineOption(QStringLiteral("no-") + option, + disabledDescription)); + disabledModuleOptions.append(disabledOption); + parser->addOption(*disabledOption.data()); + } + + const bool success = parser->parse(arguments); + if (parser->isSet(helpOption)) + return CommandLineParseHelpRequested; + if (!success) { + *errorMessage = parser->errorText(); + return CommandLineParseError; + } + + options->libraryDirectory = parser->value(libDirOption); + options->pluginDirectory = parser->value(pluginDirOption); + options->plugins = !parser->isSet(noPluginsOption); + options->libraries = !parser->isSet(noLibraryOption); + options->translations = !parser->isSet(noTranslationOption); + if (parser->isSet(translationOption)) + options->languages = parser->value(translationOption).split(QLatin1Char(',')); + options->systemD3dCompiler = !parser->isSet(noSystemD3DCompilerOption); + options->quickImports = !parser->isSet(noQuickImportOption); + + // default to deployment of compiler runtime for windows desktop configurations + if (options->platform == WindowsDesktopMinGW || options->platform == WindowsDesktopMsvc + || parser->isSet(compilerRunTimeOption)) + options->compilerRunTime = true; + if (parser->isSet(noCompilerRunTimeOption)) + options->compilerRunTime = false; + + if (options->compilerRunTime && options->platform != WindowsDesktopMinGW && options->platform != WindowsDesktopMsvc) { + *errorMessage = QStringLiteral("Deployment of the compiler runtime is implemented for Desktop MSVC/g++ only."); + return CommandLineParseError; + } + + if (parser->isSet(noVirtualKeyboardOption)) + options->disabledPlugins |= QtVirtualKeyboardPlugin; + + if (parser->isSet(releaseWithDebugInfoOption)) + std::wcerr << "Warning: " << releaseWithDebugInfoOption.names().first() << " is obsolete."; + + switch (parseExclusiveOptions(parser, debugOption, releaseOption)) { + case OptionAuto: + break; + case OptionEnabled: + options->debugDetection = Options::DebugDetectionForceDebug; + break; + case OptionDisabled: + options->debugDetection = Options::DebugDetectionForceRelease; + break; + } + + if (parser->isSet(deployPdbOption)) { + if (options->platform.testFlag(WindowsBased) && !options->platform.testFlag(MinGW)) + options->deployPdb = true; + else + std::wcerr << "Warning: --" << deployPdbOption.names().first() << " is not supported on this platform.\n"; + } + + if (parser->isSet(suppressSoftwareRasterizerOption)) + options->softwareRasterizer = false; + + if (parser->isSet(forceOption)) + options->updateFileFlags |= ForceUpdateFile; + if (parser->isSet(dryRunOption)) { + options->dryRun = true; + options->updateFileFlags |= SkipUpdateFile; + } + + options->patchQt = !parser->isSet(noPatchQtOption); + options->ignoreLibraryErrors = parser->isSet(ignoreErrorOption); + + for (int i = 0; i < qtModulesCount; ++i) { + if (parser->isSet(*enabledModuleOptions.at(i))) + options->additionalLibraries |= qtModuleEntries[i].module; + if (parser->isSet(*disabledModuleOptions.at(i))) + options->disabledLibraries |= qtModuleEntries[i].module; + } + + // Add some dependencies + if (options->additionalLibraries & QtQuickModule) + options->additionalLibraries |= QtQmlModule; + if (options->additionalLibraries & QtDesignerComponents) + options->additionalLibraries |= QtDesignerModule; + + if (parser->isSet(listOption)) { + const QString value = parser->value(listOption); + if (value == QStringLiteral("source")) { + options->list = ListSource; + } else if (value == QStringLiteral("target")) { + options->list = ListTarget; + } else if (value == QStringLiteral("relative")) { + options->list = ListRelative; + } else if (value == QStringLiteral("mapping")) { + options->list = ListMapping; + } else { + *errorMessage = QStringLiteral("Please specify a valid option for -list (source, target, relative, mapping)."); + return CommandLineParseError; + } + } + + if (parser->isSet(jsonOption) || options->list) { + optVerboseLevel = 0; + options->json = new JsonOutput; + } else { + if (parser->isSet(verboseOption)) { + bool ok; + const QString value = parser->value(verboseOption); + optVerboseLevel = value.toInt(&ok); + if (!ok || optVerboseLevel < 0) { + *errorMessage = QStringLiteral("Invalid value \"%1\" passed for verbose level.").arg(value); + return CommandLineParseError; + } + } + } + + const QStringList posArgs = parser->positionalArguments(); + if (posArgs.isEmpty()) { + *errorMessage = QStringLiteral("Please specify the binary or folder."); + return CommandLineParseError | CommandLineParseHelpRequested; + } + + if (parser->isSet(dirOption)) + options->directory = parser->value(dirOption); + + if (parser->isSet(qmakeOption) && parser->isSet(qtpathsOption)) { + *errorMessage = QStringLiteral("-qmake and -qtpaths are mutually exclusive."); + return CommandLineParseError; + } + + if (parser->isSet(qmakeOption) && optVerboseLevel >= 1) + std::wcerr << "Warning: -qmake option is deprecated. Use -qpaths instead.\n"; + + if (parser->isSet(qtpathsOption) || parser->isSet(qmakeOption)) { + const QString qtpathsArg = parser->isSet(qtpathsOption) ? parser->value(qtpathsOption) + : parser->value(qmakeOption); + + const QString qtpathsBinary = QDir::cleanPath(qtpathsArg); + const QFileInfo fi(qtpathsBinary); + if (!fi.exists()) { + *errorMessage = msgFileDoesNotExist(qtpathsBinary); + return CommandLineParseError; + } + + if (!fi.isExecutable()) { + *errorMessage = QLatin1Char('"') + QDir::toNativeSeparators(qtpathsBinary) + + QStringLiteral("\" is not an executable."); + return CommandLineParseError; + } + options->qtpathsBinary = qtpathsBinary; + } + + if (parser->isSet(qmlDirOption)) + options->qmlDirectories = parser->values(qmlDirOption); + + if (parser->isSet(qmlImportOption)) + options->qmlImportPaths = parser->values(qmlImportOption); + + const QString &file = posArgs.front(); + const QFileInfo fi(QDir::cleanPath(file)); + if (!fi.exists()) { + *errorMessage = msgFileDoesNotExist(file); + return CommandLineParseError; + } + + if (!options->directory.isEmpty() && !fi.isFile()) { // -dir was specified - expecting file. + *errorMessage = QLatin1Char('"') + file + QStringLiteral("\" is not an executable file."); + return CommandLineParseError; + } + + if (fi.isFile()) { + options->binaries.append(fi.absoluteFilePath()); + if (options->directory.isEmpty()) + options->directory = fi.absolutePath(); + } else { + const QString binary = findBinary(fi.absoluteFilePath(), options->platform); + if (binary.isEmpty()) { + *errorMessage = QStringLiteral("Unable to find binary in \"") + file + QLatin1Char('"'); + return CommandLineParseError; + } + options->directory = fi.absoluteFilePath(); + options->binaries.append(binary); + } // directory. + + // Remaining files or plugin directories + for (int i = 1; i < posArgs.size(); ++i) { + const QFileInfo fi(QDir::cleanPath(posArgs.at(i))); + const QString path = fi.absoluteFilePath(); + if (!fi.exists()) { + *errorMessage = msgFileDoesNotExist(path); + return CommandLineParseError; + } + if (fi.isDir()) { + const QStringList libraries = + findSharedLibraries(QDir(path), options->platform, MatchDebugOrRelease, QString()); + for (const QString &library : libraries) + options->binaries.append(path + QLatin1Char('/') + library); + } else { + options->binaries.append(path); + } + } + options->translationsDirectory = options->directory + QLatin1String("/translations"); + return 0; +} + +// Simple line wrapping at 80 character boundaries. +static inline QString lineBreak(QString s) +{ + for (int i = 80; i < s.size(); i += 80) { + const int lastBlank = s.lastIndexOf(QLatin1Char(' '), i); + if (lastBlank >= 0) { + s[lastBlank] = QLatin1Char('\n'); + i = lastBlank + 1; + } + } + return s; +} + +static inline QString helpText(const QCommandLineParser &p) +{ + QString result = p.helpText(); + // Replace the default-generated text which is too long by a short summary + // explaining how to enable single libraries. + const int moduleStart = result.indexOf(QLatin1String("\n --bluetooth")); + const int argumentsStart = result.lastIndexOf(QLatin1String("\nArguments:")); + if (moduleStart >= argumentsStart) + return result; + QString moduleHelp = QLatin1String( + "\n\nQt libraries can be added by passing their name (-xml) or removed by passing\n" + "the name prepended by --no- (--no-xml). Available libraries:\n"); + moduleHelp += lineBreak(QString::fromLatin1(formatQtModules(0xFFFFFFFFFFFFFFFFull, true))); + moduleHelp += QLatin1Char('\n'); + result.replace(moduleStart, argumentsStart - moduleStart, moduleHelp); + return result; +} + +static inline bool isQtModule(const QString &libName) +{ + // Match Standard modules named Qt6XX.dll + if (libName.size() < 3 || !libName.startsWith(QLatin1String("Qt"), Qt::CaseInsensitive)) + return false; + const QChar version = libName.at(2); + return version.isDigit() && (version.toLatin1() - '0') == QT_VERSION_MAJOR; +} + +// Helper for recursively finding all dependent Qt libraries. +static bool findDependentQtLibraries(const QString &qtBinDir, const QString &binary, Platform platform, + QString *errorMessage, QStringList *result, + unsigned *wordSize = nullptr, bool *isDebug = nullptr, + unsigned short *machineArch = nullptr, + int *directDependencyCount = nullptr, int recursionDepth = 0) +{ + QStringList dependentLibs; + if (directDependencyCount) + *directDependencyCount = 0; + if (!readExecutable(binary, platform, errorMessage, &dependentLibs, wordSize, isDebug, machineArch)) { + errorMessage->prepend(QLatin1String("Unable to find dependent libraries of ") + + QDir::toNativeSeparators(binary) + QLatin1String(" :")); + return false; + } + // Filter out the Qt libraries. Note that depends.exe finds libs from optDirectory if we + // are run the 2nd time (updating). We want to check against the Qt bin dir libraries + const int start = result->size(); + for (const QString &lib : qAsConst(dependentLibs)) { + if (isQtModule(lib)) { + const QString path = normalizeFileName(qtBinDir + QLatin1Char('/') + QFileInfo(lib).fileName()); + if (!result->contains(path)) + result->append(path); + } + } + const int end = result->size(); + if (directDependencyCount) + *directDependencyCount = end - start; + // Recurse + for (int i = start; i < end; ++i) + if (!findDependentQtLibraries(qtBinDir, result->at(i), platform, errorMessage, result, + nullptr, nullptr, nullptr, nullptr, recursionDepth + 1)) + return false; + return true; +} + +// Base class to filter debug/release Windows DLLs for functions to be passed to updateFile(). +// Tries to pre-filter by namefilter and does check via PE. +class DllDirectoryFileEntryFunction { +public: + explicit DllDirectoryFileEntryFunction(Platform platform, + DebugMatchMode debugMatchMode, const QString &prefix = QString()) : + m_platform(platform), m_debugMatchMode(debugMatchMode), m_prefix(prefix) {} + + QStringList operator()(const QDir &dir) const + { return findSharedLibraries(dir, m_platform, m_debugMatchMode, m_prefix); } + +private: + const Platform m_platform; + const DebugMatchMode m_debugMatchMode; + const QString m_prefix; +}; + +static QString pdbFileName(QString libraryFileName) +{ + const int lastDot = libraryFileName.lastIndexOf(QLatin1Char('.')) + 1; + if (lastDot <= 0) + return QString(); + libraryFileName.replace(lastDot, libraryFileName.size() - lastDot, QLatin1String("pdb")); + return libraryFileName; +} +static inline QStringList qmlCacheFileFilters() +{ + return QStringList() << QStringLiteral("*.jsc") << QStringLiteral("*.qmlc"); +} + +// File entry filter function for updateFile() that returns a list of files for +// QML import trees: DLLs (matching debgug) and .qml/,js, etc. +class QmlDirectoryFileEntryFunction { +public: + enum Flags { + DeployPdb = 0x1, + SkipSources = 0x2 + }; + + explicit QmlDirectoryFileEntryFunction(Platform platform, DebugMatchMode debugMatchMode, unsigned flags) + : m_flags(flags), m_qmlNameFilter(QmlDirectoryFileEntryFunction::qmlNameFilters(flags)) + , m_dllFilter(platform, debugMatchMode) + {} + + QStringList operator()(const QDir &dir) const + { + QStringList result; + const QStringList &libraries = m_dllFilter(dir); + for (const QString &library : libraries) { + result.append(library); + if (m_flags & DeployPdb) { + const QString pdb = pdbFileName(library); + if (QFileInfo(dir.absoluteFilePath(pdb)).isFile()) + result.append(pdb); + } + } + result.append(m_qmlNameFilter(dir)); + return result; + } + +private: + static inline QStringList qmlNameFilters(unsigned flags) + { + QStringList result; + result << QStringLiteral("qmldir") << QStringLiteral("*.qmltypes") + << QStringLiteral("*.frag") << QStringLiteral("*.vert") // Shaders + << QStringLiteral("*.ttf"); + if (!(flags & SkipSources)) { + result << QStringLiteral("*.js") << QStringLiteral("*.qml") << QStringLiteral("*.png"); + result.append(qmlCacheFileFilters()); + } + return result; + } + + const unsigned m_flags; + NameFilterFileEntryFunction m_qmlNameFilter; + DllDirectoryFileEntryFunction m_dllFilter; +}; + +struct PluginModuleMapping +{ + const char *directoryName; + quint64 module; +}; + +static const PluginModuleMapping pluginModuleMappings[] = +{ + {"qml1tooling", QtDeclarativeModule}, +#if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0) + {"gamepads", QtGamePadModule}, +#endif + {"accessible", QtGuiModule}, + {"iconengines", QtGuiModule}, + {"imageformats", QtGuiModule}, + {"platforms", QtGuiModule}, + {"platforminputcontexts", QtGuiModule}, + {"virtualkeyboard", QtGuiModule}, + {"geoservices", QtLocationModule}, + {"audio", QtMultimediaModule}, + {"mediaservice", QtMultimediaModule}, + {"playlistformats", QtMultimediaModule}, + {"networkaccess", QtNetworkModule}, + {"networkinformation", QtNetworkModule}, + {"tls", QtNetworkModule}, + {"position", QtPositioningModule}, + {"printsupport", QtPrintSupportModule}, + {"scenegraph", QtQuickModule}, + {"qmltooling", QtQuickModule | QtQmlToolingModule}, + {"sensors", QtSensorsModule}, + {"sensorgestures", QtSensorsModule}, + {"canbus", QtSerialBusModule}, + {"sqldrivers", QtSqlModule}, +#if QT_VERSION >= QT_VERSION_CHECK(6, 1, 0) + {"texttospeech", QtTextToSpeechModule}, +#endif + {"qtwebengine", QtWebEngineModule | QtWebEngineCoreModule | QtWebEngineWidgetsModule}, + {"styles", QtWidgetsModule}, + {"sceneparsers", Qt3DRendererModule}, + {"renderers", Qt3DRendererModule | QtShaderToolsModule}, + {"renderplugins", Qt3DRendererModule}, + {"geometryloaders", Qt3DRendererModule}, + {"webview", QtWebViewModule} +}; + +static inline quint64 qtModuleForPlugin(const QString &subDirName) +{ + const auto end = std::end(pluginModuleMappings); + const auto result = + std::find_if(std::begin(pluginModuleMappings), end, + [&subDirName] (const PluginModuleMapping &m) { return subDirName == QLatin1String(m.directoryName); }); + return result != end ? result->module : 0; // "designer" +} + +static quint64 qtModule(QString module, const QString &infix) +{ + // Match needle 'path/Qt6Core<infix><d>.dll' or 'path/libQt6Core<infix>.so.5.0' + const int lastSlashPos = module.lastIndexOf(QLatin1Char('/')); + if (lastSlashPos > 0) + module.remove(0, lastSlashPos + 1); + if (module.startsWith(QLatin1String("lib"))) + module.remove(0, 3); + int endPos = infix.isEmpty() ? -1 : module.lastIndexOf(infix); + if (endPos == -1) + endPos = module.indexOf(QLatin1Char('.')); // strip suffixes '.so.5.0'. + if (endPos > 0) + module.truncate(endPos); + // That should leave us with 'Qt6Core<d>'. + for (const auto &qtModule : qtModuleEntries) { + const QLatin1String libraryName(qtModule.libraryName); + if (module == libraryName + || (module.size() == libraryName.size() + 1 && module.startsWith(libraryName))) { + return qtModule.module; + } + } + return 0; +} + +QStringList findQtPlugins(quint64 *usedQtModules, quint64 disabledQtModules, + unsigned disabledPlugins, + const QString &qtPluginsDirName, const QString &libraryLocation, + const QString &infix, + DebugMatchMode debugMatchModeIn, Platform platform, QString *platformPlugin) +{ + QString errorMessage; + if (qtPluginsDirName.isEmpty()) + return QStringList(); + QDir pluginsDir(qtPluginsDirName); + QStringList result; + const QFileInfoList &pluginDirs = pluginsDir.entryInfoList(QStringList(QLatin1String("*")), QDir::Dirs | QDir::NoDotAndDotDot); + for (const QFileInfo &subDirFi : pluginDirs) { + const QString subDirName = subDirFi.fileName(); + const quint64 module = qtModuleForPlugin(subDirName); + if (module & *usedQtModules) { + const DebugMatchMode debugMatchMode = (module & QtWebEngineCoreModule) + ? MatchDebugOrRelease // QTBUG-44331: Debug detection does not work for webengine, deploy all. + : debugMatchModeIn; + QDir subDir(subDirFi.absoluteFilePath()); + // Filter out disabled plugins + if ((disabledPlugins & QtVirtualKeyboardPlugin) && subDirName == QLatin1String("virtualkeyboard")) + continue; + if (disabledQtModules & QtQmlToolingModule && subDirName == QLatin1String("qmltooling")) + continue; + // Filter for platform or any. + QString filter; + const bool isPlatformPlugin = subDirName == QLatin1String("platforms"); + if (isPlatformPlugin) { + switch (platform) { + case WindowsDesktopMsvc: + case WindowsDesktopMinGW: + filter = QStringLiteral("qwindows"); + break; + case Unix: + filter = QStringLiteral("libqxcb"); + break; + case UnknownPlatform: + break; + } + } else { + filter = QLatin1String("*"); + } + const QStringList plugins = findSharedLibraries(subDir, platform, debugMatchMode, filter); + for (const QString &plugin : plugins) { + // Filter out disabled plugins + if ((disabledPlugins & QtVirtualKeyboardPlugin) + && plugin.startsWith(QLatin1String("qtvirtualkeyboardplugin"))) { + continue; + } + const QString pluginPath = subDir.absoluteFilePath(plugin); + if (isPlatformPlugin) + *platformPlugin = pluginPath; + QStringList dependentQtLibs; + quint64 neededModules = 0; + if (findDependentQtLibraries(libraryLocation, pluginPath, platform, &errorMessage, &dependentQtLibs)) { + for (int d = 0; d < dependentQtLibs.size(); ++ d) + neededModules |= qtModule(dependentQtLibs.at(d), infix); + } else { + std::wcerr << "Warning: Cannot determine dependencies of " + << QDir::toNativeSeparators(pluginPath) << ": " << errorMessage << '\n'; + } + if (const quint64 missingModules = neededModules & disabledQtModules) { + if (optVerboseLevel) { + std::wcout << "Skipping plugin " << plugin + << " due to disabled dependencies (" + << formatQtModules(missingModules).constData() << ").\n"; + } + } else { + if (const quint64 missingModules = (neededModules & ~*usedQtModules)) { + *usedQtModules |= missingModules; + if (optVerboseLevel) + std::wcout << "Adding " << formatQtModules(missingModules).constData() << " for " << plugin << '\n'; + } + result.append(pluginPath); + } + } // for filter + } // type matches + } // for plugin folder + return result; +} + +static QStringList translationNameFilters(quint64 modules, const QString &prefix) +{ + QStringList result; + for (const auto &qtModule : qtModuleEntries) { + if ((qtModule.module & modules) && qtModule.translation) { + const QString name = QLatin1String(qtModule.translation) + + QLatin1Char('_') + prefix + QStringLiteral(".qm"); + if (!result.contains(name)) + result.push_back(name); + } + } + return result; +} + +static bool deployTranslations(const QString &sourcePath, quint64 usedQtModules, + const QString &target, const Options &options, + QString *errorMessage) +{ + // Find available languages prefixes by checking on qtbase. + QStringList prefixes; + QDir sourceDir(sourcePath); + const QStringList qmFilter = QStringList(QStringLiteral("qtbase_*.qm")); + const QFileInfoList &qmFiles = sourceDir.entryInfoList(qmFilter); + for (const QFileInfo &qmFi : qmFiles) { + const QString prefix = qmFi.baseName().mid(7); + if (options.languages.isEmpty() || options.languages.contains(prefix)) + prefixes.append(prefix); + } + if (prefixes.isEmpty()) { + std::wcerr << "Warning: Could not find any translations in " + << QDir::toNativeSeparators(sourcePath) << " (developer build?)\n."; + return true; + } + // Run lconvert to concatenate all files into a single named "qt_<prefix>.qm" in the application folder + // Use QT_INSTALL_TRANSLATIONS as working directory to keep the command line short. + const QString absoluteTarget = QFileInfo(target).absoluteFilePath(); + const QString binary = QStringLiteral("lconvert"); + QStringList arguments; + for (const QString &prefix : qAsConst(prefixes)) { + arguments.clear(); + const QString targetFile = QStringLiteral("qt_") + prefix + QStringLiteral(".qm"); + arguments.append(QStringLiteral("-o")); + const QString targetFilePath = absoluteTarget + QLatin1Char('/') + targetFile; + if (options.json) + options.json->addFile(sourcePath + QLatin1Char('/') + targetFile, absoluteTarget); + arguments.append(QDir::toNativeSeparators(targetFilePath)); + const QFileInfoList &langQmFiles = sourceDir.entryInfoList(translationNameFilters(usedQtModules, prefix)); + for (const QFileInfo &langQmFileFi : langQmFiles) { + if (options.json) { + options.json->addFile(langQmFileFi.absoluteFilePath(), + absoluteTarget); + } + arguments.append(langQmFileFi.fileName()); + } + if (optVerboseLevel) + std::wcout << "Creating " << targetFile << "...\n"; + unsigned long exitCode; + if ((options.updateFileFlags & SkipUpdateFile) == 0 + && (!runProcess(binary, arguments, sourcePath, &exitCode, nullptr, nullptr, errorMessage) + || exitCode)) { + return false; + } + } // for prefixes. + return true; +} + +struct DeployResult +{ + operator bool() const { return success; } + + bool success = false; + bool isDebug = false; + quint64 directlyUsedQtLibraries = 0; + quint64 usedQtLibraries = 0; + quint64 deployedQtLibraries = 0; +}; + +static QString libraryPath(const QString &libraryLocation, const char *name, + const QString &qtLibInfix, Platform platform, bool debug) +{ + QString result = libraryLocation + QLatin1Char('/'); + if (platform & WindowsBased) { + result += QLatin1String(name); + result += qtLibInfix; + if (debug && platformHasDebugSuffix(platform)) + result += QLatin1Char('d'); + } else if (platform.testFlag(UnixBased)) { + result += QStringLiteral("lib"); + result += QLatin1String(name); + result += qtLibInfix; + } + result += sharedLibrarySuffix(platform); + return result; +} + +static QString vcDebugRedistDir() { return QStringLiteral("Debug_NonRedist"); } + +static QString vcRedistDir() +{ + const char vcDirVar[] = "VCINSTALLDIR"; + const QChar slash(QLatin1Char('/')); + QString vcRedistDirName = QDir::cleanPath(QFile::decodeName(qgetenv(vcDirVar))); + if (vcRedistDirName.isEmpty()) { + std::wcerr << "Warning: Cannot find Visual Studio installation directory, " << vcDirVar + << " is not set.\n"; + return QString(); + } + if (!vcRedistDirName.endsWith(slash)) + vcRedistDirName.append(slash); + vcRedistDirName.append(QStringLiteral("redist/MSVC")); + if (!QFileInfo(vcRedistDirName).isDir()) { + std::wcerr << "Warning: Cannot find Visual Studio redist directory, " + << QDir::toNativeSeparators(vcRedistDirName).toStdWString() << ".\n"; + return QString(); + } + // Look in reverse order for folder containing the debug redist folder + const QFileInfoList subDirs = + QDir(vcRedistDirName) + .entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name | QDir::Reversed); + for (const QFileInfo &f : subDirs) { + QString path = f.absoluteFilePath(); + if (QFileInfo(path + slash + vcDebugRedistDir()).isDir()) + return path; + path += QStringLiteral("/onecore"); + if (QFileInfo(path + slash + vcDebugRedistDir()).isDir()) + return path; + } + std::wcerr << "Warning: Cannot find Visual Studio redist directory under " + << QDir::toNativeSeparators(vcRedistDirName).toStdWString() << ".\n"; + return QString(); +} + +static QStringList compilerRunTimeLibs(Platform platform, bool isDebug, unsigned short machineArch) +{ + QStringList result; + switch (platform) { + case WindowsDesktopMinGW: { // MinGW: Add runtime libraries + static const char *minGwRuntimes[] = {"*gcc_", "*stdc++", "*winpthread"}; + const QString gcc = findInPath(QStringLiteral("g++.exe")); + if (gcc.isEmpty()) { + std::wcerr << "Warning: Cannot find GCC installation directory. g++.exe must be in the path.\n"; + break; + } + const QString binPath = QFileInfo(gcc).absolutePath(); + QStringList filters; + const QString suffix = QLatin1Char('*') + sharedLibrarySuffix(platform); + for (auto minGwRuntime : minGwRuntimes) + filters.append(QLatin1String(minGwRuntime) + suffix); + const QFileInfoList &dlls = QDir(binPath).entryInfoList(filters, QDir::Files); + for (const QFileInfo &dllFi : dlls) + result.append(dllFi.absoluteFilePath()); + } + break; +#ifdef Q_OS_WIN + case WindowsDesktopMsvc: { // MSVC/Desktop: Add redistributable packages. + QString vcRedistDirName = vcRedistDir(); + if (vcRedistDirName.isEmpty()) + break; + QStringList redistFiles; + QDir vcRedistDir(vcRedistDirName); + const QString machineArchString = getArchString(machineArch); + if (isDebug) { + // Append DLLs from Debug_NonRedist\x??\Microsoft.VC<version>.DebugCRT. + if (vcRedistDir.cd(vcDebugRedistDir()) && vcRedistDir.cd(machineArchString)) { + const QStringList names = vcRedistDir.entryList(QStringList(QStringLiteral("Microsoft.VC*.DebugCRT")), QDir::Dirs); + if (!names.isEmpty() && vcRedistDir.cd(names.first())) { + const QFileInfoList &dlls = vcRedistDir.entryInfoList(QStringList(QLatin1String("*.dll"))); + for (const QFileInfo &dll : dlls) + redistFiles.append(dll.absoluteFilePath()); + } + } + } else { // release: Bundle vcredist<>.exe + QString releaseRedistDir = vcRedistDirName; + const QStringList countryCodes = vcRedistDir.entryList(QStringList(QStringLiteral("[0-9]*")), QDir::Dirs); + if (!countryCodes.isEmpty()) // Pre MSVC2017 + releaseRedistDir += QLatin1Char('/') + countryCodes.constFirst(); + QFileInfo fi(releaseRedistDir + QLatin1Char('/') + QStringLiteral("vc_redist.") + + machineArchString + QStringLiteral(".exe")); + if (!fi.isFile()) { // Pre MSVC2017/15.5 + fi.setFile(releaseRedistDir + QLatin1Char('/') + QStringLiteral("vcredist_") + + machineArchString + QStringLiteral(".exe")); + } + if (fi.isFile()) + redistFiles.append(fi.absoluteFilePath()); + } + if (redistFiles.isEmpty()) { + std::wcerr << "Warning: Cannot find Visual Studio " << (isDebug ? "debug" : "release") + << " redistributable files in " << QDir::toNativeSeparators(vcRedistDirName).toStdWString() << ".\n"; + break; + } + result.append(redistFiles); + } + break; +#endif // Q_OS_WIN + default: + break; + } + return result; +} + +static inline int qtVersion(const QMap<QString, QString> &qtpathsVariables) +{ + const QString versionString = qtpathsVariables.value(QStringLiteral("QT_VERSION")); + const QChar dot = QLatin1Char('.'); + const int majorVersion = versionString.section(dot, 0, 0).toInt(); + const int minorVersion = versionString.section(dot, 1, 1).toInt(); + const int patchVersion = versionString.section(dot, 2, 2).toInt(); + return (majorVersion << 16) | (minorVersion << 8) | patchVersion; +} + +// Determine the Qt lib infix from the library path of "Qt6Core<qtblibinfix>[d].dll". +static inline QString qtlibInfixFromCoreLibName(const QString &path, bool isDebug, Platform platform) +{ + const int startPos = path.lastIndexOf(QLatin1Char('/')) + 8; + int endPos = path.lastIndexOf(QLatin1Char('.')); + if (isDebug && (platform & WindowsBased)) + endPos--; + return endPos > startPos ? path.mid(startPos, endPos - startPos) : QString(); +} + +// Deploy a library along with its .pdb debug info file (MSVC) should it exist. +static bool updateLibrary(const QString &sourceFileName, const QString &targetDirectory, + const Options &options, QString *errorMessage) +{ + if (!updateFile(sourceFileName, targetDirectory, options.updateFileFlags, options.json, errorMessage)) { + if (options.ignoreLibraryErrors) { + std::wcerr << "Warning: Could not update " << sourceFileName << " :" << *errorMessage << '\n'; + errorMessage->clear(); + return true; + } + return false; + } + + if (options.deployPdb) { + const QFileInfo pdb(pdbFileName(sourceFileName)); + if (pdb.isFile()) + return updateFile(pdb.absoluteFilePath(), targetDirectory, options.updateFileFlags, nullptr, errorMessage); + } + return true; +} + +// Find out the ICU version to add the data library icudtXX.dll, which does not +// show as a dependency. +static QString getIcuVersion(const QString &libName) +{ + QString version; + std::copy_if(libName.cbegin(), libName.cend(), std::back_inserter(version), + [](QChar c) { return c.isDigit(); }); + return version; +} + +static DeployResult deploy(const Options &options, const QMap<QString, QString> &qtpathsVariables, + QString *errorMessage) +{ + DeployResult result; + + const QChar slash = QLatin1Char('/'); + + const QString qtBinDir = qtpathsVariables.value(QStringLiteral("QT_INSTALL_BINS")); + const QString libraryLocation = options.platform == Unix + ? qtpathsVariables.value(QStringLiteral("QT_INSTALL_LIBS")) + : qtBinDir; + const QString infix = qtpathsVariables.value(QLatin1String(qmakeInfixKey)); + const int version = qtVersion(qtpathsVariables); + Q_UNUSED(version); + + if (optVerboseLevel > 1) + std::wcout << "Qt binaries in " << QDir::toNativeSeparators(qtBinDir) << '\n'; + + QStringList dependentQtLibs; + bool detectedDebug; + unsigned wordSize; + unsigned short machineArch; + int directDependencyCount = 0; + if (!findDependentQtLibraries(libraryLocation, options.binaries.first(), options.platform, errorMessage, &dependentQtLibs, &wordSize, + &detectedDebug, &machineArch, &directDependencyCount)) { + return result; + } + for (int b = 1; b < options.binaries.size(); ++b) { + if (!findDependentQtLibraries(libraryLocation, options.binaries.at(b), options.platform, errorMessage, &dependentQtLibs, + nullptr, nullptr, nullptr)) { + return result; + } + } + + DebugMatchMode debugMatchMode = MatchDebugOrRelease; + result.isDebug = false; + switch (options.debugDetection) { + case Options::DebugDetectionAuto: + // Debug detection is only relevant for Msvc/ClangMsvc which have distinct + // runtimes and binaries. For anything else, use MatchDebugOrRelease + // since also debug cannot be reliably detect for MinGW. + if (options.platform.testFlag(Msvc) || options.platform.testFlag(ClangMsvc)) { + result.isDebug = detectedDebug; + debugMatchMode = result.isDebug ? MatchDebug : MatchRelease; + } + break; + case Options::DebugDetectionForceDebug: + result.isDebug = true; + debugMatchMode = MatchDebug; + break; + case Options::DebugDetectionForceRelease: + debugMatchMode = MatchRelease; + break; + } + + // Determine application type, check Quick2 is used by looking at the + // direct dependencies (do not be fooled by QtWebKit depending on it). + QString qtLibInfix; + for (int m = 0; m < directDependencyCount; ++m) { + const quint64 module = qtModule(dependentQtLibs.at(m), infix); + result.directlyUsedQtLibraries |= module; + if (module == QtCoreModule) + qtLibInfix = qtlibInfixFromCoreLibName(dependentQtLibs.at(m), detectedDebug, options.platform); + } + + const bool usesQml2 = !(options.disabledLibraries & QtQmlModule) + && ((result.directlyUsedQtLibraries & (QtQmlModule | QtQuickModule | Qt3DQuickModule)) + || (options.additionalLibraries & QtQmlModule)); + + if (optVerboseLevel) { + std::wcout << QDir::toNativeSeparators(options.binaries.first()) << ' ' + << wordSize << " bit, " << (result.isDebug ? "debug" : "release") + << " executable"; + if (usesQml2) + std::wcout << " [QML]"; + std::wcout << '\n'; + } + + if (dependentQtLibs.isEmpty()) { + *errorMessage = QDir::toNativeSeparators(options.binaries.first()) + QStringLiteral(" does not seem to be a Qt executable."); + return result; + } + + // Some Windows-specific checks: Qt5Core depends on ICU when configured with "-icu". Other than + // that, Qt5WebKit has a hard dependency on ICU. + if (options.platform.testFlag(WindowsBased)) { + const QStringList qtLibs = dependentQtLibs.filter(QStringLiteral("Qt6Core"), Qt::CaseInsensitive) + + dependentQtLibs.filter(QStringLiteral("Qt5WebKit"), Qt::CaseInsensitive); + for (const QString &qtLib : qtLibs) { + QStringList icuLibs = findDependentLibraries(qtLib, options.platform, errorMessage).filter(QStringLiteral("ICU"), Qt::CaseInsensitive); + if (!icuLibs.isEmpty()) { + // Find out the ICU version to add the data library icudtXX.dll, which does not show + // as a dependency. + const QString icuVersion = getIcuVersion(icuLibs.constFirst()); + if (!icuVersion.isEmpty()) { + if (optVerboseLevel > 1) + std::wcout << "Adding ICU version " << icuVersion << '\n'; + QString icuLib = QStringLiteral("icudt") + icuVersion + + QLatin1String(windowsSharedLibrarySuffix);; + // Some packages contain debug dlls of ICU libraries even though it's a C + // library and the official packages do not differentiate (QTBUG-87677) + if (result.isDebug) { + const QString icuLibCandidate = QStringLiteral("icudtd") + icuVersion + + QLatin1String(windowsSharedLibrarySuffix); + if (!findInPath(icuLibCandidate).isEmpty()) { + icuLib = icuLibCandidate; + } + } + icuLibs.push_back(icuLib); + } + for (const QString &icuLib : qAsConst(icuLibs)) { + const QString icuPath = findInPath(icuLib); + if (icuPath.isEmpty()) { + *errorMessage = QStringLiteral("Unable to locate ICU library ") + icuLib; + return result; + } + dependentQtLibs.push_back(icuPath); + } // for each icuLib + break; + } // !icuLibs.isEmpty() + } // Qt6Core/Qt6WebKit + } // Windows + + // Scan Quick2 imports + QmlImportScanResult qmlScanResult; + if (options.quickImports && usesQml2) { + // Custom list of import paths provided by user + QStringList qmlImportPaths = options.qmlImportPaths; + // Qt's own QML modules + qmlImportPaths << qtpathsVariables.value(QStringLiteral("QT_INSTALL_QML")); + QStringList qmlDirectories = options.qmlDirectories; + if (qmlDirectories.isEmpty()) { + const QString qmlDirectory = findQmlDirectory(options.platform, options.directory); + if (!qmlDirectory.isEmpty()) + qmlDirectories.append(qmlDirectory); + } + for (const QString &qmlDirectory : qAsConst(qmlDirectories)) { + if (optVerboseLevel >= 1) + std::wcout << "Scanning " << QDir::toNativeSeparators(qmlDirectory) << ":\n"; + const QmlImportScanResult scanResult = + runQmlImportScanner(qmlDirectory, qmlImportPaths, + result.directlyUsedQtLibraries & QtWidgetsModule, + options.platform, debugMatchMode, errorMessage); + if (!scanResult.ok) + return result; + qmlScanResult.append(scanResult); + // Additional dependencies of QML plugins. + for (const QString &plugin : qAsConst(qmlScanResult.plugins)) { + if (!findDependentQtLibraries(libraryLocation, plugin, options.platform, errorMessage, &dependentQtLibs, &wordSize, &detectedDebug, &machineArch)) + return result; + } + if (optVerboseLevel >= 1) { + std::wcout << "QML imports:\n"; + for (const QmlImportScanResult::Module &mod : qAsConst(qmlScanResult.modules)) { + std::wcout << " '" << mod.name << "' " + << QDir::toNativeSeparators(mod.sourcePath) << '\n'; + } + if (optVerboseLevel >= 2) { + std::wcout << "QML plugins:\n"; + for (const QString &p : qAsConst(qmlScanResult.plugins)) + std::wcout << " " << QDir::toNativeSeparators(p) << '\n'; + } + } + } + } + + QString platformPlugin; + // Sort apart Qt 5 libraries in the ones that are represented by the + // QtModule enumeration (and thus controlled by flags) and others. + QStringList deployedQtLibraries; + for (int i = 0 ; i < dependentQtLibs.size(); ++i) { + if (const quint64 qtm = qtModule(dependentQtLibs.at(i), infix)) + result.usedQtLibraries |= qtm; + else + deployedQtLibraries.push_back(dependentQtLibs.at(i)); // Not represented by flag. + } + result.deployedQtLibraries = (result.usedQtLibraries | options.additionalLibraries) & ~options.disabledLibraries; + + const QStringList plugins = findQtPlugins( + &result.deployedQtLibraries, + // For non-QML applications, disable QML to prevent it from being pulled in by the + // qtaccessiblequick plugin. + options.disabledLibraries | (usesQml2 ? 0 : (QtQmlModule | QtQuickModule)), + options.disabledPlugins, qtpathsVariables.value(QStringLiteral("QT_INSTALL_PLUGINS")), + libraryLocation, infix, debugMatchMode, options.platform, &platformPlugin); + + // Apply options flags and re-add library names. + QString qtGuiLibrary; + for (const auto &qtModule : qtModuleEntries) { + if (result.deployedQtLibraries & qtModule.module) { + const QString library = libraryPath(libraryLocation, qtModule.libraryName, qtLibInfix, options.platform, result.isDebug); + deployedQtLibraries.append(library); + if (qtModule.module == QtGuiModule) + qtGuiLibrary = library; + } + } + + if (optVerboseLevel >= 1) { + std::wcout << "Direct dependencies: " << formatQtModules(result.directlyUsedQtLibraries).constData() + << "\nAll dependencies : " << formatQtModules(result.usedQtLibraries).constData() + << "\nTo be deployed : " << formatQtModules(result.deployedQtLibraries).constData() << '\n'; + } + + if (optVerboseLevel > 1) + std::wcout << "Plugins: " << plugins.join(QLatin1Char(',')) << '\n'; + + if ((result.deployedQtLibraries & QtGuiModule) && platformPlugin.isEmpty()) { + *errorMessage =QStringLiteral("Unable to find the platform plugin."); + return result; + } + + if (options.platform.testFlag(WindowsBased) && !qtGuiLibrary.isEmpty()) { + const QStringList guiLibraries = findDependentLibraries(qtGuiLibrary, options.platform, errorMessage); + const bool dependsOnOpenGl = !guiLibraries.filter(QStringLiteral("opengl32"), Qt::CaseInsensitive).isEmpty(); + if (options.softwareRasterizer && !dependsOnOpenGl) { + const QFileInfo softwareRasterizer(qtBinDir + slash + QStringLiteral("opengl32sw") + QLatin1String(windowsSharedLibrarySuffix)); + if (softwareRasterizer.isFile()) + deployedQtLibraries.append(softwareRasterizer.absoluteFilePath()); + } + if (options.systemD3dCompiler && machineArch != IMAGE_FILE_MACHINE_ARM64) { + const QString d3dCompiler = findD3dCompiler(options.platform, qtBinDir, wordSize); + if (d3dCompiler.isEmpty()) { + std::wcerr << "Warning: Cannot find any version of the d3dcompiler DLL.\n"; + } else { + deployedQtLibraries.push_back(d3dCompiler); + } + } + } // Windows + + // Update libraries + if (options.libraries) { + const QString targetPath = options.libraryDirectory.isEmpty() ? + options.directory : options.libraryDirectory; + QStringList libraries = deployedQtLibraries; + if (options.compilerRunTime) + libraries.append(compilerRunTimeLibs(options.platform, result.isDebug, machineArch)); + for (const QString &qtLib : qAsConst(libraries)) { + if (!updateLibrary(qtLib, targetPath, options, errorMessage)) + return result; + } + + if (options.patchQt && !options.dryRun) { + const QString qt6CoreName = QFileInfo(libraryPath(libraryLocation, "Qt6Core", qtLibInfix, + options.platform, result.isDebug)).fileName(); +#ifndef QT_RELOCATABLE + if (!patchQtCore(targetPath + QLatin1Char('/') + qt6CoreName, errorMessage)) { + std::wcerr << "Warning: " << *errorMessage << '\n'; + errorMessage->clear(); + } +#endif + } + } // optLibraries + + // Update plugins + if (options.plugins) { + const QString targetPath = options.pluginDirectory.isEmpty() ? + options.directory : options.pluginDirectory; + QDir dir(targetPath); + if (!dir.exists() && !dir.mkpath(QStringLiteral("."))) { + *errorMessage = QLatin1String("Cannot create ") + + QDir::toNativeSeparators(dir.absolutePath()) + QLatin1Char('.'); + return result; + } + for (const QString &plugin : plugins) { + const QString targetDirName = plugin.section(slash, -2, -2); + const QString targetPath = dir.absoluteFilePath(targetDirName); + if (!dir.exists(targetDirName)) { + if (optVerboseLevel) + std::wcout << "Creating directory " << targetPath << ".\n"; + if (!(options.updateFileFlags & SkipUpdateFile) && !dir.mkdir(targetDirName)) { + *errorMessage = QStringLiteral("Cannot create ") + targetDirName + QLatin1Char('.'); + return result; + } + } + if (!updateLibrary(plugin, targetPath, options, errorMessage)) + return result; + } + } // optPlugins + + // Update Quick imports + const bool usesQuick1 = result.deployedQtLibraries & QtDeclarativeModule; + // Do not be fooled by QtWebKit.dll depending on Quick into always installing Quick imports + // for WebKit1-applications. Check direct dependency only. + if (options.quickImports && (usesQuick1 || usesQml2)) { + if (usesQml2) { + for (const QmlImportScanResult::Module &module : qAsConst(qmlScanResult.modules)) { + const QString installPath = module.installPath(options.directory); + if (optVerboseLevel > 1) + std::wcout << "Installing: '" << module.name + << "' from " << module.sourcePath << " to " + << QDir::toNativeSeparators(installPath) << '\n'; + if (installPath != options.directory && !createDirectory(installPath, errorMessage)) + return result; + unsigned updateFileFlags = options.updateFileFlags | SkipQmlDesignerSpecificsDirectories; + unsigned qmlDirectoryFileFlags = 0; + if (options.deployPdb) + qmlDirectoryFileFlags |= QmlDirectoryFileEntryFunction::DeployPdb; + if (!updateFile(module.sourcePath, QmlDirectoryFileEntryFunction(options.platform, debugMatchMode, qmlDirectoryFileFlags), + installPath, updateFileFlags, options.json, errorMessage)) { + return result; + } + } + } // Quick 2 + if (usesQuick1) { + const QString quick1ImportPath = + qtpathsVariables.value(QStringLiteral("QT_INSTALL_IMPORTS")); + const QmlDirectoryFileEntryFunction qmlFileEntryFunction(options.platform, debugMatchMode, options.deployPdb ? QmlDirectoryFileEntryFunction::DeployPdb : 0); + QStringList quick1Imports(QStringLiteral("Qt")); + for (const QString &quick1Import : qAsConst(quick1Imports)) { + const QString sourceFile = quick1ImportPath + slash + quick1Import; + if (!updateFile(sourceFile, qmlFileEntryFunction, options.directory, options.updateFileFlags, options.json, errorMessage)) + return result; + } + } // Quick 1 + } // optQuickImports + + if (options.translations) { + if (!options.dryRun && !createDirectory(options.translationsDirectory, errorMessage)) + return result; + if (!deployTranslations(qtpathsVariables.value(QStringLiteral("QT_INSTALL_TRANSLATIONS")), + result.deployedQtLibraries, options.translationsDirectory, options, + errorMessage)) { + return result; + } + } + + result.success = true; + return result; +} + +static bool deployWebProcess(const QMap<QString, QString> &qtpathsVariables, const char *binaryName, + const Options &sourceOptions, QString *errorMessage) +{ + // Copy the web process and its dependencies + const QString webProcess = webProcessBinary(binaryName, sourceOptions.platform); + const QString webProcessSource = qtpathsVariables.value(QStringLiteral("QT_INSTALL_LIBEXECS")) + + QLatin1Char('/') + webProcess; + if (!updateFile(webProcessSource, sourceOptions.directory, sourceOptions.updateFileFlags, sourceOptions.json, errorMessage)) + return false; + Options options(sourceOptions); + options.binaries.append(options.directory + QLatin1Char('/') + webProcess); + options.quickImports = false; + options.translations = false; + return deploy(options, qtpathsVariables, errorMessage); +} + +static bool deployWebEngineCore(const QMap<QString, QString> &qtpathsVariables, + const Options &options, bool isDebug, QString *errorMessage) +{ + static const char *installDataFiles[] = {"icudtl.dat", + "qtwebengine_devtools_resources.pak", + "qtwebengine_resources.pak", + "qtwebengine_resources_100p.pak", + "qtwebengine_resources_200p.pak"}; + QByteArray webEngineProcessName(webEngineProcessC); + if (isDebug && platformHasDebugSuffix(options.platform)) + webEngineProcessName.append('d'); + if (optVerboseLevel) + std::wcout << "Deploying: " << webEngineProcessName.constData() << "...\n"; + if (!deployWebProcess(qtpathsVariables, webEngineProcessName, options, errorMessage)) + return false; + const QString resourcesSubDir = QStringLiteral("/resources"); + const QString resourcesSourceDir = qtpathsVariables.value(QStringLiteral("QT_INSTALL_DATA")) + + resourcesSubDir + QLatin1Char('/'); + const QString resourcesTargetDir(options.directory + resourcesSubDir); + if (!createDirectory(resourcesTargetDir, errorMessage)) + return false; + for (auto installDataFile : installDataFiles) { + if (!updateFile(resourcesSourceDir + QLatin1String(installDataFile), + resourcesTargetDir, options.updateFileFlags, options.json, errorMessage)) { + return false; + } + } + const QFileInfo translations(qtpathsVariables.value(QStringLiteral("QT_INSTALL_TRANSLATIONS")) + + QStringLiteral("/qtwebengine_locales")); + if (!translations.isDir()) { + std::wcerr << "Warning: Cannot find the translation files of the QtWebEngine module at " + << QDir::toNativeSeparators(translations.absoluteFilePath()) << ".\n"; + return true; + } + if (options.translations) { + // Copy the whole translations directory. + return createDirectory(options.translationsDirectory, errorMessage) + && updateFile(translations.absoluteFilePath(), options.translationsDirectory, + options.updateFileFlags, options.json, errorMessage); + } + // Translations have been turned off, but QtWebEngine needs at least one. + const QFileInfo enUSpak(translations.filePath() + QStringLiteral("/en-US.pak")); + if (!enUSpak.exists()) { + std::wcerr << "Warning: Cannot find " + << QDir::toNativeSeparators(enUSpak.absoluteFilePath()) << ".\n"; + return true; + } + const QString webEngineTranslationsDir = options.translationsDirectory + QLatin1Char('/') + + translations.fileName(); + if (!createDirectory(webEngineTranslationsDir, errorMessage)) + return false; + return updateFile(enUSpak.absoluteFilePath(), webEngineTranslationsDir, + options.updateFileFlags, options.json, errorMessage); +} + +QT_END_NAMESPACE + +QT_USE_NAMESPACE + +int main(int argc, char **argv) +{ + QCoreApplication a(argc, argv); + QCoreApplication::setApplicationVersion(QLatin1String(QT_VERSION_STR)); + + const QByteArray qtBinPath = QFile::encodeName(QDir::toNativeSeparators(QCoreApplication::applicationDirPath())); + QByteArray path = qgetenv("PATH"); + if (!path.contains(qtBinPath)) { // QTBUG-39177, ensure Qt is in the path so that qt.conf is taken into account. + path += ';'; + path += qtBinPath; + qputenv("PATH", path); + } + + Options options; + QString errorMessage; + + { // Command line + QCommandLineParser parser; + QString errorMessage; + const int result = parseArguments(QCoreApplication::arguments(), &parser, &options, &errorMessage); + if (result & CommandLineParseError) + std::wcerr << errorMessage << "\n\n"; + if (result & CommandLineParseHelpRequested) + std::fputs(qPrintable(helpText(parser)), stdout); + if (result & CommandLineParseError) + return 1; + if (result & CommandLineParseHelpRequested) + return 0; + } + + const QMap<QString, QString> qtpathsVariables = + queryQtPaths(options.qtpathsBinary, &errorMessage); + const QString xSpec = qtpathsVariables.value(QStringLiteral("QMAKE_XSPEC")); + options.platform = platformFromMkSpec(xSpec); + + if (qtpathsVariables.isEmpty() || xSpec.isEmpty() + || !qtpathsVariables.contains(QStringLiteral("QT_INSTALL_BINS"))) { + std::wcerr << "Unable to query qtpaths: " << errorMessage << '\n'; + return 1; + } + + if (options.platform == UnknownPlatform) { + std::wcerr << "Unsupported platform " << xSpec << '\n'; + return 1; + } + + // Create directories + if (!createDirectory(options.directory, &errorMessage)) { + std::wcerr << errorMessage << '\n'; + return 1; + } + if (!options.libraryDirectory.isEmpty() && options.libraryDirectory != options.directory + && !createDirectory(options.libraryDirectory, &errorMessage)) { + std::wcerr << errorMessage << '\n'; + return 1; + } + + const DeployResult result = deploy(options, qtpathsVariables, &errorMessage); + if (!result) { + std::wcerr << errorMessage << '\n'; + return 1; + } + + if (result.deployedQtLibraries & QtWebEngineCoreModule) { + if (!deployWebEngineCore(qtpathsVariables, options, result.isDebug, &errorMessage)) { + std::wcerr << errorMessage << '\n'; + return 1; + } + } + + if (options.json) { + if (options.list) + std::fputs(options.json->toList(options.list, options.directory).constData(), stdout); + else + std::fputs(options.json->toJson().constData(), stdout); + delete options.json; + options.json = nullptr; + } + + return 0; +} diff --git a/src/tools/windeployqt/qmlutils.cpp b/src/tools/windeployqt/qmlutils.cpp new file mode 100644 index 0000000000..6eebf6d86e --- /dev/null +++ b/src/tools/windeployqt/qmlutils.cpp @@ -0,0 +1,160 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** 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 "qmlutils.h" +#include "utils.h" + +#include <QtCore/QDir> +#include <QtCore/QFileInfo> +#include <QtCore/QCoreApplication> +#include <QtCore/QJsonDocument> +#include <QtCore/QJsonObject> +#include <QtCore/QJsonArray> +#include <QtCore/QJsonParseError> + +QT_BEGIN_NAMESPACE + +bool operator==(const QmlImportScanResult::Module &m1, const QmlImportScanResult::Module &m2) +{ + return m1.className.isEmpty() ? m1.name == m2.name : m1.className == m2.className; +} + +// Return install path (cp -r semantics) +QString QmlImportScanResult::Module::installPath(const QString &root) const +{ + QString result = root; + const int lastSlashPos = relativePath.lastIndexOf(QLatin1Char('/')); + if (lastSlashPos != -1) { + result += QLatin1Char('/'); + result += QStringView{relativePath}.left(lastSlashPos); + } + return result; +} + +static QString qmlDirectoryRecursion(Platform platform, const QString &path) +{ + QDir dir(path); + if (!dir.entryList(QStringList(QStringLiteral("*.qml")), QDir::Files, QDir::NoSort).isEmpty()) + return dir.path(); + const QFileInfoList &subDirs = dir.entryInfoList(QStringList(), QDir::Dirs | QDir::NoDotAndDotDot, QDir::NoSort); + for (const QFileInfo &subDirFi : subDirs) { + if (!isBuildDirectory(platform, subDirFi.fileName())) { + const QString subPath = qmlDirectoryRecursion(platform, subDirFi.absoluteFilePath()); + if (!subPath.isEmpty()) + return subPath; + } + } + return QString(); +} + +// Find a directory containing QML files in the project +QString findQmlDirectory(Platform platform, const QString &startDirectoryName) +{ + QDir startDirectory(startDirectoryName); + if (isBuildDirectory(platform, startDirectory.dirName())) + startDirectory.cdUp(); + return qmlDirectoryRecursion(platform, startDirectory.path()); +} + +static void findFileRecursion(const QDir &directory, Platform platform, + DebugMatchMode debugMatchMode, QStringList *matches) +{ + const QStringList &dlls = findSharedLibraries(directory, platform, debugMatchMode); + for (const QString &dll : dlls) + matches->append(directory.filePath(dll)); + const QFileInfoList &subDirs = directory.entryInfoList(QStringList(), QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks); + for (const QFileInfo &subDirFi : subDirs) { + QDir subDirectory(subDirFi.absoluteFilePath()); + if (subDirectory.isReadable()) + findFileRecursion(subDirectory, platform, debugMatchMode, matches); + } +} + +QmlImportScanResult runQmlImportScanner(const QString &directory, const QStringList &qmlImportPaths, + bool usesWidgets, int platform, DebugMatchMode debugMatchMode, + QString *errorMessage) +{ + Q_UNUSED(usesWidgets); + QmlImportScanResult result; + QStringList arguments; + for (const QString &importPath : qmlImportPaths) + arguments << QStringLiteral("-importPath") << importPath; + arguments << QStringLiteral("-rootPath") << directory; + unsigned long exitCode; + QByteArray stdOut; + QByteArray stdErr; + const QString binary = QStringLiteral("qmlimportscanner"); + if (!runProcess(binary, arguments, QDir::currentPath(), &exitCode, &stdOut, &stdErr, errorMessage)) + return result; + if (exitCode) { + *errorMessage = binary + QStringLiteral(" returned ") + QString::number(exitCode) + + QStringLiteral(": ") + QString::fromLocal8Bit(stdErr); + return result; + } + QJsonParseError jsonParseError{}; + const QJsonDocument data = QJsonDocument::fromJson(stdOut, &jsonParseError); + if (data.isNull() ) { + *errorMessage = binary + QStringLiteral(" returned invalid JSON output: ") + + jsonParseError.errorString() + QStringLiteral(" :\"") + + QString::fromLocal8Bit(stdOut) + QLatin1Char('"'); + return result; + } + const QJsonArray array = data.array(); + const int childCount = array.count(); + for (int c = 0; c < childCount; ++c) { + const QJsonObject object = array.at(c).toObject(); + if (object.value(QStringLiteral("type")).toString() == QLatin1String("module")) { + const QString path = object.value(QStringLiteral("path")).toString(); + if (!path.isEmpty()) { + QmlImportScanResult::Module module; + module.name = object.value(QStringLiteral("name")).toString(); + module.className = object.value(QStringLiteral("classname")).toString(); + module.sourcePath = path; + module.relativePath = object.value(QStringLiteral("relativePath")).toString(); + result.modules.append(module); + findFileRecursion(QDir(path), Platform(platform), debugMatchMode, &result.plugins); + } + } + } + result.ok = true; + return result; +} + +void QmlImportScanResult::append(const QmlImportScanResult &other) +{ + for (const QmlImportScanResult::Module &module : other.modules) { + if (std::find(modules.cbegin(), modules.cend(), module) == modules.cend()) + modules.append(module); + } + for (const QString &plugin : other.plugins) { + if (!plugins.contains(plugin)) + plugins.append(plugin); + } +} + +QT_END_NAMESPACE diff --git a/src/tools/windeployqt/qmlutils.h b/src/tools/windeployqt/qmlutils.h new file mode 100644 index 0000000000..572be7cab0 --- /dev/null +++ b/src/tools/windeployqt/qmlutils.h @@ -0,0 +1,65 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** 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 QMLUTILS_H +#define QMLUTILS_H + +#include "utils.h" + +#include <QStringList> + +QT_BEGIN_NAMESPACE + +QString findQmlDirectory(Platform platform, const QString &startDirectoryName); + +struct QmlImportScanResult { + struct Module { + QString installPath(const QString &root) const; + + QString name; + QString className; + QString sourcePath; + QString relativePath; + }; + + void append(const QmlImportScanResult &other); + + bool ok = false; + QList<Module> modules; + QStringList plugins; +}; + +bool operator==(const QmlImportScanResult::Module &m1, const QmlImportScanResult::Module &m2); + +QmlImportScanResult runQmlImportScanner(const QString &directory, const QStringList &qmlImportPaths, + bool usesWidgets, int platform, DebugMatchMode debugMatchMode, + QString *errorMessage); + +QT_END_NAMESPACE + +#endif // QMLUTILS_H diff --git a/src/tools/windeployqt/utils.cpp b/src/tools/windeployqt/utils.cpp new file mode 100644 index 0000000000..bc52de924e --- /dev/null +++ b/src/tools/windeployqt/utils.cpp @@ -0,0 +1,1006 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** 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 "utils.h" +#include "elfreader.h" + +#include <QtCore/QString> +#include <QtCore/QDebug> +#include <QtCore/QDir> +#include <QtCore/QFile> +#include <QtCore/QFileInfo> +#include <QtCore/QTemporaryFile> +#include <QtCore/QScopedPointer> +#include <QtCore/QScopedArrayPointer> +#include <QtCore/QStandardPaths> +#if defined(Q_OS_WIN) +# include <QtCore/qt_windows.h> +# include <shlwapi.h> +# include <delayimp.h> +#else // Q_OS_WIN +# include <sys/wait.h> +# include <sys/types.h> +# include <sys/stat.h> +# include <unistd.h> +# include <stdlib.h> +# include <string.h> +# include <errno.h> +# include <fcntl.h> +#endif // !Q_OS_WIN + +QT_BEGIN_NAMESPACE + +int optVerboseLevel = 1; + +bool isBuildDirectory(Platform platform, const QString &dirName) +{ + return (platform.testFlag(Msvc) || platform.testFlag(ClangMsvc)) + && (dirName == QLatin1String("debug") || dirName == QLatin1String("release")); +} + +// Create a symbolic link by changing to the source directory to make sure the +// link uses relative paths only (QFile::link() otherwise uses the absolute path). +bool createSymbolicLink(const QFileInfo &source, const QString &target, QString *errorMessage) +{ + const QString oldDirectory = QDir::currentPath(); + if (!QDir::setCurrent(source.absolutePath())) { + *errorMessage = QStringLiteral("Unable to change to directory %1.").arg(QDir::toNativeSeparators(source.absolutePath())); + return false; + } + QFile file(source.fileName()); + const bool success = file.link(target); + QDir::setCurrent(oldDirectory); + if (!success) { + *errorMessage = QString::fromLatin1("Failed to create symbolic link %1 -> %2: %3") + .arg(QDir::toNativeSeparators(source.absoluteFilePath()), + QDir::toNativeSeparators(target), file.errorString()); + return false; + } + return true; +} + +bool createDirectory(const QString &directory, QString *errorMessage) +{ + const QFileInfo fi(directory); + if (fi.isDir()) + return true; + if (fi.exists()) { + *errorMessage = QString::fromLatin1("%1 already exists and is not a directory."). + arg(QDir::toNativeSeparators(directory)); + return false; + } + if (optVerboseLevel) + std::wcout << "Creating " << QDir::toNativeSeparators(directory) << "...\n"; + QDir dir; + if (!dir.mkpath(directory)) { + *errorMessage = QString::fromLatin1("Could not create directory %1."). + arg(QDir::toNativeSeparators(directory)); + return false; + } + return true; +} + +// Find shared libraries matching debug/Platform in a directory, return relative names. +QStringList findSharedLibraries(const QDir &directory, Platform platform, + DebugMatchMode debugMatchMode, + const QString &prefix) +{ + QString nameFilter = prefix; + if (nameFilter.isEmpty()) + nameFilter += QLatin1Char('*'); + if (debugMatchMode == MatchDebug && platformHasDebugSuffix(platform)) + nameFilter += QLatin1Char('d'); + nameFilter += sharedLibrarySuffix(platform); + QStringList result; + QString errorMessage; + const QFileInfoList &dlls = directory.entryInfoList(QStringList(nameFilter), QDir::Files); + for (const QFileInfo &dllFi : dlls) { + const QString dllPath = dllFi.absoluteFilePath(); + bool matches = true; + if (debugMatchMode != MatchDebugOrRelease && (platform & WindowsBased)) { + bool debugDll; + if (readPeExecutable(dllPath, &errorMessage, 0, 0, &debugDll, + (platform == WindowsDesktopMinGW))) { + matches = debugDll == (debugMatchMode == MatchDebug); + } else { + std::wcerr << "Warning: Unable to read " << QDir::toNativeSeparators(dllPath) + << ": " << errorMessage; + } + } // Windows + if (matches) + result += dllFi.fileName(); + } // for + return result; +} + +#ifdef Q_OS_WIN +QString winErrorMessage(unsigned long error) +{ + QString rc = QString::fromLatin1("#%1: ").arg(error); + char16_t *lpMsgBuf; + + const DWORD len = FormatMessage( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, error, 0, reinterpret_cast<LPTSTR>(&lpMsgBuf), 0, NULL); + if (len) { + rc = QString::fromUtf16(lpMsgBuf, int(len)); + LocalFree(lpMsgBuf); + } else { + rc += QString::fromLatin1("<unknown error>"); + } + return rc; +} + +// Case-Normalize file name via GetShortPathNameW()/GetLongPathNameW() +QString normalizeFileName(const QString &name) +{ + wchar_t shortBuffer[MAX_PATH]; + const QString nativeFileName = QDir::toNativeSeparators(name); + if (!GetShortPathNameW(reinterpret_cast<LPCWSTR>(nativeFileName.utf16()), shortBuffer, MAX_PATH)) + return name; + wchar_t result[MAX_PATH]; + if (!GetLongPathNameW(shortBuffer, result, MAX_PATH)) + return name; + return QDir::fromNativeSeparators(QString::fromWCharArray(result)); +} + +// Find a tool binary in the Windows SDK 8 +QString findSdkTool(const QString &tool) +{ + QStringList paths = QString::fromLocal8Bit(qgetenv("PATH")).split(QLatin1Char(';')); + const QByteArray sdkDir = qgetenv("WindowsSdkDir"); + if (!sdkDir.isEmpty()) + paths.prepend(QDir::cleanPath(QString::fromLocal8Bit(sdkDir)) + QLatin1String("/Tools/x64")); + return QStandardPaths::findExecutable(tool, paths); +} + +// runProcess helper: Create a temporary file for stdout/stderr redirection. +static HANDLE createInheritableTemporaryFile() +{ + wchar_t path[MAX_PATH]; + if (!GetTempPath(MAX_PATH, path)) + return INVALID_HANDLE_VALUE; + wchar_t name[MAX_PATH]; + if (!GetTempFileName(path, L"temp", 0, name)) // Creates file. + return INVALID_HANDLE_VALUE; + SECURITY_ATTRIBUTES securityAttributes; + ZeroMemory(&securityAttributes, sizeof(securityAttributes)); + securityAttributes.nLength = sizeof(securityAttributes); + securityAttributes.bInheritHandle = TRUE; + return CreateFile(name, GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, &securityAttributes, + TRUNCATE_EXISTING, + FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE, NULL); +} + +// runProcess helper: Rewind and read out a temporary file for stdout/stderr. +static inline void readTemporaryProcessFile(HANDLE handle, QByteArray *result) +{ + if (SetFilePointer(handle, 0, 0, FILE_BEGIN) == 0xFFFFFFFF) + return; + char buf[1024]; + DWORD bytesRead; + while (ReadFile(handle, buf, sizeof(buf), &bytesRead, NULL) && bytesRead) + result->append(buf, int(bytesRead)); + CloseHandle(handle); +} + +static inline void appendToCommandLine(const QString &argument, QString *commandLine) +{ + const bool needsQuote = argument.contains(QLatin1Char(' ')); + if (!commandLine->isEmpty()) + commandLine->append(QLatin1Char(' ')); + if (needsQuote) + commandLine->append(QLatin1Char('"')); + commandLine->append(argument); + if (needsQuote) + commandLine->append(QLatin1Char('"')); +} + +// runProcess: Run a command line process (replacement for QProcess which +// does not exist in the bootstrap library). +bool runProcess(const QString &binary, const QStringList &args, + const QString &workingDirectory, + unsigned long *exitCode, QByteArray *stdOut, QByteArray *stdErr, + QString *errorMessage) +{ + if (exitCode) + *exitCode = 0; + + STARTUPINFO si; + ZeroMemory(&si, sizeof(si)); + si.cb = sizeof(si); + + STARTUPINFO myInfo; + GetStartupInfo(&myInfo); + si.hStdInput = myInfo.hStdInput; + si.hStdOutput = myInfo.hStdOutput; + si.hStdError = myInfo.hStdError; + + PROCESS_INFORMATION pi; + ZeroMemory(&pi, sizeof(PROCESS_INFORMATION)); + const QChar backSlash = QLatin1Char('\\'); + QString nativeWorkingDir = QDir::toNativeSeparators(workingDirectory.isEmpty() ? QDir::currentPath() : workingDirectory); + if (!nativeWorkingDir.endsWith(backSlash)) + nativeWorkingDir += backSlash; + + if (stdOut) { + si.hStdOutput = createInheritableTemporaryFile(); + if (si.hStdOutput == INVALID_HANDLE_VALUE) { + if (errorMessage) + *errorMessage = QStringLiteral("Error creating stdout temporary file"); + return false; + } + si.dwFlags |= STARTF_USESTDHANDLES; + } + + if (stdErr) { + si.hStdError = createInheritableTemporaryFile(); + if (si.hStdError == INVALID_HANDLE_VALUE) { + if (errorMessage) + *errorMessage = QStringLiteral("Error creating stderr temporary file"); + return false; + } + si.dwFlags |= STARTF_USESTDHANDLES; + } + + // Create a copy of the command line which CreateProcessW can modify. + QString commandLine; + appendToCommandLine(binary, &commandLine); + for (const QString &a : args) + appendToCommandLine(a, &commandLine); + if (optVerboseLevel > 1) + std::wcout << "Running: " << commandLine << '\n'; + + QScopedArrayPointer<wchar_t> commandLineW(new wchar_t[commandLine.size() + 1]); + commandLine.toWCharArray(commandLineW.data()); + commandLineW[commandLine.size()] = 0; + if (!CreateProcessW(0, commandLineW.data(), 0, 0, /* InheritHandles */ TRUE, 0, 0, + reinterpret_cast<LPCWSTR>(nativeWorkingDir.utf16()), &si, &pi)) { + if (stdOut) + CloseHandle(si.hStdOutput); + if (stdErr) + CloseHandle(si.hStdError); + if (errorMessage) + *errorMessage = QStringLiteral("CreateProcessW failed: ") + winErrorMessage(GetLastError()); + return false; + } + + WaitForSingleObject(pi.hProcess, INFINITE); + CloseHandle(pi.hThread); + if (exitCode) + GetExitCodeProcess(pi.hProcess, exitCode); + CloseHandle(pi.hProcess); + + if (stdOut) + readTemporaryProcessFile(si.hStdOutput, stdOut); + if (stdErr) + readTemporaryProcessFile(si.hStdError, stdErr); + return true; +} + +#else // Q_OS_WIN + +static inline char *encodeFileName(const QString &f) +{ + const QByteArray encoded = QFile::encodeName(f); + char *result = new char[encoded.size() + 1]; + strcpy(result, encoded.constData()); + return result; +} + +static inline char *tempFilePattern() +{ + QString path = QDir::tempPath(); + if (!path.endsWith(QLatin1Char('/'))) + path += QLatin1Char('/'); + path += QStringLiteral("tmpXXXXXX"); + return encodeFileName(path); +} + +static inline QByteArray readOutRedirectFile(int fd) +{ + enum { bufSize = 256 }; + + QByteArray result; + if (!lseek(fd, 0, 0)) { + char buf[bufSize]; + while (true) { + const ssize_t rs = read(fd, buf, bufSize); + if (rs <= 0) + break; + result.append(buf, int(rs)); + } + } + close(fd); + return result; +} + +// runProcess: Run a command line process (replacement for QProcess which +// does not exist in the bootstrap library). +bool runProcess(const QString &binary, const QStringList &args, + const QString &workingDirectory, + unsigned long *exitCode, QByteArray *stdOut, QByteArray *stdErr, + QString *errorMessage) +{ + QScopedArrayPointer<char> stdOutFileName; + QScopedArrayPointer<char> stdErrFileName; + + int stdOutFile = 0; + if (stdOut) { + stdOutFileName.reset(tempFilePattern()); + stdOutFile = mkstemp(stdOutFileName.data()); + if (stdOutFile < 0) { + *errorMessage = QStringLiteral("mkstemp() failed: ") + QString::fromLocal8Bit(strerror(errno)); + return false; + } + } + + int stdErrFile = 0; + if (stdErr) { + stdErrFileName.reset(tempFilePattern()); + stdErrFile = mkstemp(stdErrFileName.data()); + if (stdErrFile < 0) { + *errorMessage = QStringLiteral("mkstemp() failed: ") + QString::fromLocal8Bit(strerror(errno)); + return false; + } + } + + const pid_t pID = fork(); + + if (pID < 0) { + *errorMessage = QStringLiteral("Fork failed: ") + QString::fromLocal8Bit(strerror(errno)); + return false; + } + + if (!pID) { // Child + if (stdOut) { + dup2(stdOutFile, STDOUT_FILENO); + close(stdOutFile); + } + if (stdErr) { + dup2(stdErrFile, STDERR_FILENO); + close(stdErrFile); + } + + if (!workingDirectory.isEmpty() && !QDir::setCurrent(workingDirectory)) { + std::wcerr << "Failed to change working directory to " << workingDirectory << ".\n"; + ::_exit(-1); + } + + char **argv = new char *[args.size() + 2]; // Create argv. + char **ap = argv; + *ap++ = encodeFileName(binary); + for (const QString &a : qAsConst(args)) + *ap++ = encodeFileName(a); + *ap = 0; + + execvp(argv[0], argv); + ::_exit(-1); + } + + int status; + pid_t waitResult; + + do { + waitResult = waitpid(pID, &status, 0); + } while (waitResult == -1 && errno == EINTR); + + if (stdOut) { + *stdOut = readOutRedirectFile(stdOutFile); + unlink(stdOutFileName.data()); + } + if (stdErr) { + *stdErr = readOutRedirectFile(stdErrFile); + unlink(stdErrFileName.data()); + } + + if (waitResult < 0) { + *errorMessage = QStringLiteral("Wait failed: ") + QString::fromLocal8Bit(strerror(errno)); + return false; + } + if (!WIFEXITED(status)) { + *errorMessage = binary + QStringLiteral(" did not exit cleanly."); + return false; + } + if (exitCode) + *exitCode = WEXITSTATUS(status); + return true; +} + +#endif // !Q_OS_WIN + +// Find a file in the path using ShellAPI. This can be used to locate DLLs which +// QStandardPaths cannot do. +QString findInPath(const QString &file) +{ +#if defined(Q_OS_WIN) + if (file.size() < MAX_PATH - 1) { + wchar_t buffer[MAX_PATH]; + file.toWCharArray(buffer); + buffer[file.size()] = 0; + if (PathFindOnPath(buffer, NULL)) + return QDir::cleanPath(QString::fromWCharArray(buffer)); + } + return QString(); +#else // Q_OS_WIN + return QStandardPaths::findExecutable(file); +#endif // !Q_OS_WIN +} + +const char *qmakeInfixKey = "QT_INFIX"; + +QMap<QString, QString> queryQtPaths(const QString &qtpathsBinary, QString *errorMessage) +{ + const QString binary = !qtpathsBinary.isEmpty() ? qtpathsBinary : QStringLiteral("qtpaths"); + QByteArray stdOut; + QByteArray stdErr; + unsigned long exitCode = 0; + if (!runProcess(binary, QStringList(QStringLiteral("-query")), QString(), &exitCode, &stdOut, &stdErr, errorMessage)) + return QMap<QString, QString>(); + if (exitCode) { + *errorMessage = binary + QStringLiteral(" returns ") + QString::number(exitCode) + + QStringLiteral(": ") + QString::fromLocal8Bit(stdErr); + return QMap<QString, QString>(); + } + const QString output = QString::fromLocal8Bit(stdOut).trimmed().remove(QLatin1Char('\r')); + QMap<QString, QString> result; + const int size = output.size(); + for (int pos = 0; pos < size; ) { + const int colonPos = output.indexOf(QLatin1Char(':'), pos); + if (colonPos < 0) + break; + int endPos = output.indexOf(QLatin1Char('\n'), colonPos + 1); + if (endPos < 0) + endPos = size; + const QString key = output.mid(pos, colonPos - pos); + const QString value = output.mid(colonPos + 1, endPos - colonPos - 1); + result.insert(key, value); + pos = endPos + 1; + } + QFile qconfigPriFile(result.value(QStringLiteral("QT_HOST_DATA")) + QStringLiteral("/mkspecs/qconfig.pri")); + if (qconfigPriFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + while (true) { + const QByteArray line = qconfigPriFile.readLine(); + if (line.isEmpty()) + break; + if (line.startsWith("QT_LIBINFIX")) { + const int pos = line.indexOf('='); + if (pos >= 0) { + const QString infix = QString::fromUtf8(line.right(line.size() - pos - 1).trimmed()); + if (!infix.isEmpty()) + result.insert(QLatin1String(qmakeInfixKey), infix); + } + break; + } + } + } else { + std::wcerr << "Warning: Unable to read " << QDir::toNativeSeparators(qconfigPriFile.fileName()) + << ": " << qconfigPriFile.errorString()<< '\n'; + } + return result; +} + +// Update a file or directory. +bool updateFile(const QString &sourceFileName, const QStringList &nameFilters, + const QString &targetDirectory, unsigned flags, JsonOutput *json, QString *errorMessage) +{ + const QFileInfo sourceFileInfo(sourceFileName); + const QString targetFileName = targetDirectory + QLatin1Char('/') + sourceFileInfo.fileName(); + if (optVerboseLevel > 1) + std::wcout << "Checking " << sourceFileName << ", " << targetFileName<< '\n'; + + if (!sourceFileInfo.exists()) { + *errorMessage = QString::fromLatin1("%1 does not exist.").arg(QDir::toNativeSeparators(sourceFileName)); + return false; + } + + if (sourceFileInfo.isSymLink()) { + *errorMessage = QString::fromLatin1("Symbolic links are not supported (%1).") + .arg(QDir::toNativeSeparators(sourceFileName)); + return false; + } + + const QFileInfo targetFileInfo(targetFileName); + + if (sourceFileInfo.isDir()) { + if (targetFileInfo.exists()) { + if (!targetFileInfo.isDir()) { + *errorMessage = QString::fromLatin1("%1 already exists and is not a directory.") + .arg(QDir::toNativeSeparators(targetFileName)); + return false; + } // Not a directory. + } else { // exists. + QDir d(targetDirectory); + if (optVerboseLevel) + std::wcout << "Creating " << QDir::toNativeSeparators(targetFileName) << ".\n"; + if (!(flags & SkipUpdateFile) && !d.mkdir(sourceFileInfo.fileName())) { + *errorMessage = QString::fromLatin1("Cannot create directory %1 under %2.") + .arg(sourceFileInfo.fileName(), QDir::toNativeSeparators(targetDirectory)); + return false; + } + } + // Recurse into directory + QDir dir(sourceFileName); + const QFileInfoList allEntries = dir.entryInfoList(nameFilters, QDir::Files) + + dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); + for (const QFileInfo &entryFi : allEntries) { + if (!updateFile(entryFi.absoluteFilePath(), nameFilters, targetFileName, flags, json, errorMessage)) + return false; + } + return true; + } // Source is directory. + + if (targetFileInfo.exists()) { + if (!(flags & ForceUpdateFile) + && targetFileInfo.lastModified() >= sourceFileInfo.lastModified()) { + if (optVerboseLevel) + std::wcout << sourceFileInfo.fileName() << " is up to date.\n"; + if (json) + json->addFile(sourceFileName, targetDirectory); + return true; + } + QFile targetFile(targetFileName); + if (!(flags & SkipUpdateFile) && !targetFile.remove()) { + *errorMessage = QString::fromLatin1("Cannot remove existing file %1: %2") + .arg(QDir::toNativeSeparators(targetFileName), targetFile.errorString()); + return false; + } + } // target exists + QFile file(sourceFileName); + if (optVerboseLevel) + std::wcout << "Updating " << sourceFileInfo.fileName() << ".\n"; + if (!(flags & SkipUpdateFile) && !file.copy(targetFileName)) { + *errorMessage = QString::fromLatin1("Cannot copy %1 to %2: %3") + .arg(QDir::toNativeSeparators(sourceFileName), + QDir::toNativeSeparators(targetFileName), + file.errorString()); + return false; + } + if (json) + json->addFile(sourceFileName, targetDirectory); + return true; +} + +bool readElfExecutable(const QString &elfExecutableFileName, QString *errorMessage, + QStringList *dependentLibraries, unsigned *wordSize, + bool *isDebug) +{ + ElfReader elfReader(elfExecutableFileName); + const ElfData data = elfReader.readHeaders(); + if (data.sectionHeaders.isEmpty()) { + *errorMessage = QStringLiteral("Unable to read ELF binary \"") + + QDir::toNativeSeparators(elfExecutableFileName) + QStringLiteral("\": ") + + elfReader.errorString(); + return false; + } + if (wordSize) + *wordSize = data.elfclass == Elf_ELFCLASS64 ? 64 : 32; + if (dependentLibraries) { + dependentLibraries->clear(); + const QList<QByteArray> libs = elfReader.dependencies(); + if (libs.isEmpty()) { + *errorMessage = QStringLiteral("Unable to read dependenices of ELF binary \"") + + QDir::toNativeSeparators(elfExecutableFileName) + QStringLiteral("\": ") + + elfReader.errorString(); + return false; + } + for (const QByteArray &l : libs) + dependentLibraries->push_back(QString::fromLocal8Bit(l)); + } + if (isDebug) + *isDebug = data.symbolsType != UnknownSymbols && data.symbolsType != NoSymbols; + return true; +} + +#ifdef Q_OS_WIN + +static inline QString stringFromRvaPtr(const void *rvaPtr) +{ + return QString::fromLocal8Bit(static_cast<const char *>(rvaPtr)); +} + +// Helper for reading out PE executable files: Find a section header for an RVA +// (IMAGE_NT_HEADERS64, IMAGE_NT_HEADERS32). +template <class ImageNtHeader> +const IMAGE_SECTION_HEADER *findSectionHeader(DWORD rva, const ImageNtHeader *nTHeader) +{ + const IMAGE_SECTION_HEADER *section = IMAGE_FIRST_SECTION(nTHeader); + const IMAGE_SECTION_HEADER *sectionEnd = section + nTHeader->FileHeader.NumberOfSections; + for ( ; section < sectionEnd; ++section) + if (rva >= section->VirtualAddress && rva < (section->VirtualAddress + section->Misc.VirtualSize)) + return section; + return 0; +} + +// Helper for reading out PE executable files: convert RVA to pointer (IMAGE_NT_HEADERS64, IMAGE_NT_HEADERS32). +template <class ImageNtHeader> +inline const void *rvaToPtr(DWORD rva, const ImageNtHeader *nTHeader, const void *imageBase) +{ + const IMAGE_SECTION_HEADER *sectionHdr = findSectionHeader(rva, nTHeader); + if (!sectionHdr) + return 0; + const DWORD delta = sectionHdr->VirtualAddress - sectionHdr->PointerToRawData; + return static_cast<const char *>(imageBase) + rva - delta; +} + +// Helper for reading out PE executable files: return word size of a IMAGE_NT_HEADERS64, IMAGE_NT_HEADERS32 +template <class ImageNtHeader> +inline unsigned ntHeaderWordSize(const ImageNtHeader *header) +{ + // defines IMAGE_NT_OPTIONAL_HDR32_MAGIC, IMAGE_NT_OPTIONAL_HDR64_MAGIC + enum { imageNtOptionlHeader32Magic = 0x10b, imageNtOptionlHeader64Magic = 0x20b }; + if (header->OptionalHeader.Magic == imageNtOptionlHeader32Magic) + return 32; + if (header->OptionalHeader.Magic == imageNtOptionlHeader64Magic) + return 64; + return 0; +} + +// Helper for reading out PE executable files: Retrieve the NT image header of an +// executable via the legacy DOS header. +static IMAGE_NT_HEADERS *getNtHeader(void *fileMemory, QString *errorMessage) +{ + IMAGE_DOS_HEADER *dosHeader = static_cast<PIMAGE_DOS_HEADER>(fileMemory); + // Check DOS header consistency + if (IsBadReadPtr(dosHeader, sizeof(IMAGE_DOS_HEADER)) + || dosHeader->e_magic != IMAGE_DOS_SIGNATURE) { + *errorMessage = QString::fromLatin1("DOS header check failed."); + return 0; + } + // Retrieve NT header + char *ntHeaderC = static_cast<char *>(fileMemory) + dosHeader->e_lfanew; + IMAGE_NT_HEADERS *ntHeaders = reinterpret_cast<IMAGE_NT_HEADERS *>(ntHeaderC); + // check NT header consistency + if (IsBadReadPtr(ntHeaders, sizeof(ntHeaders->Signature)) + || ntHeaders->Signature != IMAGE_NT_SIGNATURE + || IsBadReadPtr(&ntHeaders->FileHeader, sizeof(IMAGE_FILE_HEADER))) { + *errorMessage = QString::fromLatin1("NT header check failed."); + return 0; + } + // Check magic + if (!ntHeaderWordSize(ntHeaders)) { + *errorMessage = QString::fromLatin1("NT header check failed; magic %1 is invalid."). + arg(ntHeaders->OptionalHeader.Magic); + return 0; + } + // Check section headers + IMAGE_SECTION_HEADER *sectionHeaders = IMAGE_FIRST_SECTION(ntHeaders); + if (IsBadReadPtr(sectionHeaders, ntHeaders->FileHeader.NumberOfSections * sizeof(IMAGE_SECTION_HEADER))) { + *errorMessage = QString::fromLatin1("NT header section header check failed."); + return 0; + } + return ntHeaders; +} + +// Helper for reading out PE executable files: Read out import sections from +// IMAGE_NT_HEADERS64, IMAGE_NT_HEADERS32. +template <class ImageNtHeader> +inline QStringList readImportSections(const ImageNtHeader *ntHeaders, const void *base, QString *errorMessage) +{ + // Get import directory entry RVA and read out + const DWORD importsStartRVA = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress; + if (!importsStartRVA) { + *errorMessage = QString::fromLatin1("Failed to find IMAGE_DIRECTORY_ENTRY_IMPORT entry."); + return QStringList(); + } + const IMAGE_IMPORT_DESCRIPTOR *importDesc = static_cast<const IMAGE_IMPORT_DESCRIPTOR *>(rvaToPtr(importsStartRVA, ntHeaders, base)); + if (!importDesc) { + *errorMessage = QString::fromLatin1("Failed to find IMAGE_IMPORT_DESCRIPTOR entry."); + return QStringList(); + } + QStringList result; + for ( ; importDesc->Name; ++importDesc) + result.push_back(stringFromRvaPtr(rvaToPtr(importDesc->Name, ntHeaders, base))); + + // Read delay-loaded DLLs, see http://msdn.microsoft.com/en-us/magazine/cc301808.aspx . + // Check on grAttr bit 1 whether this is the format using RVA's > VS 6 + if (const DWORD delayedImportsStartRVA = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT].VirtualAddress) { + const ImgDelayDescr *delayedImportDesc = static_cast<const ImgDelayDescr *>(rvaToPtr(delayedImportsStartRVA, ntHeaders, base)); + for ( ; delayedImportDesc->rvaDLLName && (delayedImportDesc->grAttrs & 1); ++delayedImportDesc) + result.push_back(stringFromRvaPtr(rvaToPtr(delayedImportDesc->rvaDLLName, ntHeaders, base))); + } + + return result; +} + +// Check for MSCV runtime (MSVCP90D.dll/MSVCP90.dll, MSVCP120D.dll/MSVCP120.dll, +// VCRUNTIME140D.DLL/VCRUNTIME140.DLL (VS2015) or msvcp120d_app.dll/msvcp120_app.dll). +enum MsvcDebugRuntimeResult { MsvcDebugRuntime, MsvcReleaseRuntime, NoMsvcRuntime }; + +static inline MsvcDebugRuntimeResult checkMsvcDebugRuntime(const QStringList &dependentLibraries) +{ + for (const QString &lib : dependentLibraries) { + int pos = 0; + if (lib.startsWith(QLatin1String("MSVCR"), Qt::CaseInsensitive) + || lib.startsWith(QLatin1String("MSVCP"), Qt::CaseInsensitive) + || lib.startsWith(QLatin1String("VCRUNTIME"), Qt::CaseInsensitive)) { + int lastDotPos = lib.lastIndexOf(QLatin1Char('.')); + pos = -1 == lastDotPos ? 0 : lastDotPos - 1; + } + + if (pos > 0 && lib.contains(QLatin1String("_app"), Qt::CaseInsensitive)) + pos -= 4; + + if (pos) { + return lib.at(pos).toLower() == QLatin1Char('d') + ? MsvcDebugRuntime : MsvcReleaseRuntime; + } + } + return NoMsvcRuntime; +} + +template <class ImageNtHeader> +inline void determineDebugAndDependentLibs(const ImageNtHeader *nth, const void *fileMemory, + bool isMinGW, + QStringList *dependentLibrariesIn, + bool *isDebugIn, QString *errorMessage) +{ + const bool hasDebugEntry = nth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG].Size; + QStringList dependentLibraries; + if (dependentLibrariesIn || (isDebugIn != nullptr && hasDebugEntry && !isMinGW)) + dependentLibraries = readImportSections(nth, fileMemory, errorMessage); + + if (dependentLibrariesIn) + *dependentLibrariesIn = dependentLibraries; + if (isDebugIn != nullptr) { + if (isMinGW) { + // Use logic that's used e.g. in objdump / pfd library + *isDebugIn = !(nth->FileHeader.Characteristics & IMAGE_FILE_DEBUG_STRIPPED); + } else { + // When an MSVC debug entry is present, check whether the debug runtime + // is actually used to detect -release / -force-debug-info builds. + *isDebugIn = hasDebugEntry && checkMsvcDebugRuntime(dependentLibraries) != MsvcReleaseRuntime; + } + } +} + +// Read a PE executable and determine dependent libraries, word size +// and debug flags. +bool readPeExecutable(const QString &peExecutableFileName, QString *errorMessage, + QStringList *dependentLibrariesIn, unsigned *wordSizeIn, + bool *isDebugIn, bool isMinGW, unsigned short *machineArchIn) +{ + bool result = false; + HANDLE hFile = NULL; + HANDLE hFileMap = NULL; + void *fileMemory = 0; + + if (dependentLibrariesIn) + dependentLibrariesIn->clear(); + if (wordSizeIn) + *wordSizeIn = 0; + if (isDebugIn) + *isDebugIn = false; + + do { + // Create a memory mapping of the file + hFile = CreateFile(reinterpret_cast<const WCHAR*>(peExecutableFileName.utf16()), GENERIC_READ, FILE_SHARE_READ, NULL, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (hFile == INVALID_HANDLE_VALUE || hFile == NULL) { + *errorMessage = QString::fromLatin1("Cannot open '%1': %2").arg(peExecutableFileName, winErrorMessage(GetLastError())); + break; + } + + hFileMap = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL); + if (hFileMap == NULL) { + *errorMessage = QString::fromLatin1("Cannot create file mapping of '%1': %2").arg(peExecutableFileName, winErrorMessage(GetLastError())); + break; + } + + fileMemory = MapViewOfFile(hFileMap, FILE_MAP_READ, 0, 0, 0); + if (!fileMemory) { + *errorMessage = QString::fromLatin1("Cannot map '%1': %2").arg(peExecutableFileName, winErrorMessage(GetLastError())); + break; + } + + const IMAGE_NT_HEADERS *ntHeaders = getNtHeader(fileMemory, errorMessage); + if (!ntHeaders) + break; + + const unsigned wordSize = ntHeaderWordSize(ntHeaders); + if (wordSizeIn) + *wordSizeIn = wordSize; + if (wordSize == 32) { + determineDebugAndDependentLibs(reinterpret_cast<const IMAGE_NT_HEADERS32 *>(ntHeaders), + fileMemory, isMinGW, dependentLibrariesIn, isDebugIn, errorMessage); + } else { + determineDebugAndDependentLibs(reinterpret_cast<const IMAGE_NT_HEADERS64 *>(ntHeaders), + fileMemory, isMinGW, dependentLibrariesIn, isDebugIn, errorMessage); + } + + if (machineArchIn) + *machineArchIn = ntHeaders->FileHeader.Machine; + + result = true; + if (optVerboseLevel > 1) { + std::wcout << __FUNCTION__ << ": " << QDir::toNativeSeparators(peExecutableFileName) + << ' ' << wordSize << " bit"; + if (isMinGW) + std::wcout << ", MinGW"; + if (dependentLibrariesIn) { + std::wcout << ", dependent libraries: "; + if (optVerboseLevel > 2) + std::wcout << dependentLibrariesIn->join(QLatin1Char(' ')); + else + std::wcout << dependentLibrariesIn->size(); + } + if (isDebugIn) + std::wcout << (*isDebugIn ? ", debug" : ", release"); + std::wcout << '\n'; + } + } while (false); + + if (fileMemory) + UnmapViewOfFile(fileMemory); + + if (hFileMap != NULL) + CloseHandle(hFileMap); + + if (hFile != NULL && hFile != INVALID_HANDLE_VALUE) + CloseHandle(hFile); + + return result; +} + +QString findD3dCompiler(Platform platform, const QString &qtBinDir, unsigned wordSize) +{ + const QString prefix = QStringLiteral("D3Dcompiler_"); + const QString suffix = QLatin1String(windowsSharedLibrarySuffix); + // Get the DLL from Kit 8.0 onwards + const QString kitDir = QString::fromLocal8Bit(qgetenv("WindowsSdkDir")); + if (!kitDir.isEmpty()) { + QString redistDirPath = QDir::cleanPath(kitDir) + QStringLiteral("/Redist/D3D/"); + if (platform.testFlag(ArmBased)) { + redistDirPath += QStringLiteral("arm"); + } else { + redistDirPath += wordSize == 32 ? QStringLiteral("x86") : QStringLiteral("x64"); + } + QDir redistDir(redistDirPath); + if (redistDir.exists()) { + const QFileInfoList files = redistDir.entryInfoList(QStringList(prefix + QLatin1Char('*') + suffix), QDir::Files); + if (!files.isEmpty()) + return files.front().absoluteFilePath(); + } + } + QStringList candidateVersions; + for (int i = 47 ; i >= 40 ; --i) + candidateVersions.append(prefix + QString::number(i) + suffix); + // Check the bin directory of the Qt SDK (in case it is shadowed by the + // Windows system directory in PATH). + for (const QString &candidate : qAsConst(candidateVersions)) { + const QFileInfo fi(qtBinDir + QLatin1Char('/') + candidate); + if (fi.isFile()) + return fi.absoluteFilePath(); + } + // Find the latest D3D compiler DLL in path (Windows 8.1 has d3dcompiler_47). + if (platform.testFlag(IntelBased)) { + QString errorMessage; + unsigned detectedWordSize; + for (const QString &candidate : qAsConst(candidateVersions)) { + const QString dll = findInPath(candidate); + if (!dll.isEmpty() + && readPeExecutable(dll, &errorMessage, 0, &detectedWordSize, 0) + && detectedWordSize == wordSize) { + return dll; + } + } + } + return QString(); +} + +#else // Q_OS_WIN + +bool readPeExecutable(const QString &, QString *errorMessage, + QStringList *, unsigned *, bool *, bool, unsigned short *) +{ + *errorMessage = QStringLiteral("Not implemented."); + return false; +} + +QString findD3dCompiler(Platform, const QString &, unsigned) +{ + return QString(); +} + +#endif // !Q_OS_WIN + +// Search for "qt_prfxpath=xxxx" in \a path, and replace it with "qt_prfxpath=." +bool patchQtCore(const QString &path, QString *errorMessage) +{ + if (optVerboseLevel) + std::wcout << "Patching " << QFileInfo(path).fileName() << "...\n"; + + QFile file(path); + if (!file.open(QIODevice::ReadOnly)) { + *errorMessage = QString::fromLatin1("Unable to patch %1: %2").arg( + QDir::toNativeSeparators(path), file.errorString()); + return false; + } + const QByteArray oldContent = file.readAll(); + + if (oldContent.isEmpty()) { + *errorMessage = QString::fromLatin1("Unable to patch %1: Could not read file content").arg( + QDir::toNativeSeparators(path)); + return false; + } + file.close(); + + QByteArray content = oldContent; + + QByteArray prfxpath("qt_prfxpath="); + int startPos = content.indexOf(prfxpath); + if (startPos == -1) { + *errorMessage = QString::fromLatin1( + "Unable to patch %1: Could not locate pattern \"qt_prfxpath=\"").arg( + QDir::toNativeSeparators(path)); + return false; + } + startPos += prfxpath.length(); + int endPos = content.indexOf(char(0), startPos); + if (endPos == -1) { + *errorMessage = QString::fromLatin1("Unable to patch %1: Internal error").arg( + QDir::toNativeSeparators(path)); + return false; + } + + QByteArray replacement = QByteArray(endPos - startPos, char(0)); + replacement[0] = '.'; + content.replace(startPos, endPos - startPos, replacement); + if (content == oldContent) + return true; + + if (!file.open(QIODevice::WriteOnly) + || (file.write(content) != content.size())) { + *errorMessage = QString::fromLatin1("Unable to patch %1: Could not write to file: %2").arg( + QDir::toNativeSeparators(path), file.errorString()); + return false; + } + return true; +} + +#ifdef Q_OS_WIN +QString getArchString(unsigned short machineArch) +{ + switch (machineArch) { + case IMAGE_FILE_MACHINE_I386: + return QStringLiteral("x86"); + case IMAGE_FILE_MACHINE_ARM: + return QStringLiteral("arm"); + case IMAGE_FILE_MACHINE_AMD64: + return QStringLiteral("x64"); + case IMAGE_FILE_MACHINE_ARM64: + return QStringLiteral("arm64"); + default: + break; + } + return QString(); +} +#endif // Q_OS_WIN + +QT_END_NAMESPACE diff --git a/src/tools/windeployqt/utils.h b/src/tools/windeployqt/utils.h new file mode 100644 index 0000000000..0fe3a6948f --- /dev/null +++ b/src/tools/windeployqt/utils.h @@ -0,0 +1,404 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** 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 UTILS_H +#define UTILS_H + +#include <QStringList> +#include <QMap> +#include <QtCore/QFile> +#include <QtCore/QDir> +#include <QtCore/QDateTime> +#include <QtCore/QJsonArray> +#include <QtCore/QJsonObject> +#include <QtCore/QJsonDocument> + +#include <iostream> + +QT_BEGIN_NAMESPACE + +enum PlatformFlag { + // OS + WindowsBased = 0x00001, + UnixBased = 0x00002, + // CPU + IntelBased = 0x00010, + ArmBased = 0x00020, + // Compiler + Msvc = 0x00100, + MinGW = 0x00200, + ClangMsvc = 0x00400, + ClangMinGW = 0x00800, + // Platforms + WindowsDesktopMsvc = WindowsBased + IntelBased + Msvc, + WindowsDesktopMinGW = WindowsBased + IntelBased + MinGW, + WindowsDesktopClangMsvc = WindowsBased + IntelBased + ClangMsvc, + WindowsDesktopClangMinGW = WindowsBased + IntelBased + ClangMinGW, + Unix = UnixBased, + UnknownPlatform +}; + +Q_DECLARE_FLAGS(Platform, PlatformFlag) + +Q_DECLARE_OPERATORS_FOR_FLAGS(Platform) + +inline bool platformHasDebugSuffix(Platform p) // Uses 'd' debug suffix +{ + return p.testFlag(Msvc) || p.testFlag(ClangMsvc); +} + +enum ListOption { + ListNone = 0, + ListSource, + ListTarget, + ListRelative, + ListMapping +}; + +inline std::wostream &operator<<(std::wostream &str, const QString &s) +{ +#ifdef Q_OS_WIN + str << reinterpret_cast<const wchar_t *>(s.utf16()); +#else + str << s.toStdWString(); +#endif + return str; +} + +// Container class for JSON output +class JsonOutput +{ + using SourceTargetMapping = QPair<QString, QString>; + using SourceTargetMappings = QList<SourceTargetMapping>; + +public: + void addFile(const QString &source, const QString &target) + { + m_files.append(SourceTargetMapping(source, target)); + } + + void removeTargetDirectory(const QString &targetDirectory) + { + for (int i = m_files.size() - 1; i >= 0; --i) { + if (m_files.at(i).second == targetDirectory) + m_files.removeAt(i); + } + } + + QByteArray toJson() const + { + QJsonObject document; + QJsonArray files; + for (const SourceTargetMapping &mapping : m_files) { + QJsonObject object; + object.insert(QStringLiteral("source"), QDir::toNativeSeparators(mapping.first)); + object.insert(QStringLiteral("target"), QDir::toNativeSeparators(mapping.second)); + files.append(object); + } + document.insert(QStringLiteral("files"), files); + return QJsonDocument(document).toJson(); + } + QByteArray toList(ListOption option, const QDir &base) const + { + QByteArray list; + for (const SourceTargetMapping &mapping : m_files) { + const QString source = QDir::toNativeSeparators(mapping.first); + const QString fileName = QFileInfo(mapping.first).fileName(); + const QString target = QDir::toNativeSeparators(mapping.second) + QDir::separator() + fileName; + switch (option) { + case ListNone: + break; + case ListSource: + list += source.toUtf8() + '\n'; + break; + case ListTarget: + list += target.toUtf8() + '\n'; + break; + case ListRelative: + list += QDir::toNativeSeparators(base.relativeFilePath(target)).toUtf8() + '\n'; + break; + case ListMapping: + list += '"' + source.toUtf8() + "\" \"" + QDir::toNativeSeparators(base.relativeFilePath(target)).toUtf8() + "\"\n"; + break; + } + } + return list; + } +private: + SourceTargetMappings m_files; +}; + +#ifdef Q_OS_WIN +QString normalizeFileName(const QString &name); +QString winErrorMessage(unsigned long error); +QString findSdkTool(const QString &tool); +#else // !Q_OS_WIN +inline QString normalizeFileName(const QString &name) { return name; } +#endif // !Q_OS_WIN + +static const char windowsSharedLibrarySuffix[] = ".dll"; +static const char unixSharedLibrarySuffix[] = ".so"; + +inline QString sharedLibrarySuffix(Platform platform) { return QLatin1String((platform & WindowsBased) ? windowsSharedLibrarySuffix : unixSharedLibrarySuffix); } +bool isBuildDirectory(Platform platform, const QString &dirName); + +bool createSymbolicLink(const QFileInfo &source, const QString &target, QString *errorMessage); +bool createDirectory(const QString &directory, QString *errorMessage); +QString findInPath(const QString &file); + +extern const char *qmakeInfixKey; // Fake key containing the libinfix + +QMap<QString, QString> queryQtPaths(const QString &qmakeBinary, QString *errorMessage); + +enum DebugMatchMode { + MatchDebug, + MatchRelease, + MatchDebugOrRelease +}; + +QStringList findSharedLibraries(const QDir &directory, Platform platform, + DebugMatchMode debugMatchMode, + const QString &prefix = QString()); + +bool updateFile(const QString &sourceFileName, const QStringList &nameFilters, + const QString &targetDirectory, unsigned flags, JsonOutput *json, QString *errorMessage); +bool runProcess(const QString &binary, const QStringList &args, + const QString &workingDirectory = QString(), + unsigned long *exitCode = 0, QByteArray *stdOut = 0, QByteArray *stdErr = 0, + QString *errorMessage = 0); + +bool readPeExecutable(const QString &peExecutableFileName, QString *errorMessage, + QStringList *dependentLibraries = 0, unsigned *wordSize = 0, + bool *isDebug = 0, bool isMinGW = false, unsigned short *machineArch = nullptr); +bool readElfExecutable(const QString &elfExecutableFileName, QString *errorMessage, + QStringList *dependentLibraries = 0, unsigned *wordSize = 0, + bool *isDebug = 0); + +inline bool readExecutable(const QString &executableFileName, Platform platform, + QString *errorMessage, QStringList *dependentLibraries = 0, + unsigned *wordSize = 0, bool *isDebug = 0, unsigned short *machineArch = nullptr) +{ + return platform == Unix ? + readElfExecutable(executableFileName, errorMessage, dependentLibraries, wordSize, isDebug) : + readPeExecutable(executableFileName, errorMessage, dependentLibraries, wordSize, isDebug, + (platform == WindowsDesktopMinGW), machineArch); +} + +#ifdef Q_OS_WIN +# if !defined(IMAGE_FILE_MACHINE_ARM64) +# define IMAGE_FILE_MACHINE_ARM64 0xAA64 +# endif +QString getArchString (unsigned short machineArch); +#endif // Q_OS_WIN + +// Return dependent modules of executable files. + +inline QStringList findDependentLibraries(const QString &executableFileName, Platform platform, QString *errorMessage) +{ + QStringList result; + readExecutable(executableFileName, platform, errorMessage, &result); + return result; +} + +QString findD3dCompiler(Platform platform, const QString &qtBinDir, unsigned wordSize); + +bool patchQtCore(const QString &path, QString *errorMessage); + +extern int optVerboseLevel; + +// Recursively update a file or directory, matching DirectoryFileEntryFunction against the QDir +// to obtain the files. +enum UpdateFileFlag { + ForceUpdateFile = 0x1, + SkipUpdateFile = 0x2, + RemoveEmptyQmlDirectories = 0x4, + SkipQmlDesignerSpecificsDirectories = 0x8 +}; + +template <class DirectoryFileEntryFunction> +bool updateFile(const QString &sourceFileName, + DirectoryFileEntryFunction directoryFileEntryFunction, + const QString &targetDirectory, + unsigned flags, + JsonOutput *json, + QString *errorMessage) +{ + const QFileInfo sourceFileInfo(sourceFileName); + const QString targetFileName = targetDirectory + QLatin1Char('/') + sourceFileInfo.fileName(); + if (optVerboseLevel > 1) + std::wcout << "Checking " << sourceFileName << ", " << targetFileName << '\n'; + + if (!sourceFileInfo.exists()) { + *errorMessage = QString::fromLatin1("%1 does not exist.").arg(QDir::toNativeSeparators(sourceFileName)); + return false; + } + + const QFileInfo targetFileInfo(targetFileName); + + if (sourceFileInfo.isSymLink()) { + const QString sourcePath = sourceFileInfo.symLinkTarget(); + const QString relativeSource = QDir(sourceFileInfo.absolutePath()).relativeFilePath(sourcePath); + if (relativeSource.contains(QLatin1Char('/'))) { + *errorMessage = QString::fromLatin1("Symbolic links across directories are not supported (%1).") + .arg(QDir::toNativeSeparators(sourceFileName)); + return false; + } + + // Update the linked-to file + if (!updateFile(sourcePath, directoryFileEntryFunction, targetDirectory, flags, json, errorMessage)) + return false; + + if (targetFileInfo.exists()) { + if (!targetFileInfo.isSymLink()) { + *errorMessage = QString::fromLatin1("%1 already exists and is not a symbolic link.") + .arg(QDir::toNativeSeparators(targetFileName)); + return false; + } // Not a symlink + const QString relativeTarget = QDir(targetFileInfo.absolutePath()).relativeFilePath(targetFileInfo.symLinkTarget()); + if (relativeSource == relativeTarget) // Exists and points to same entry: happy. + return true; + QFile existingTargetFile(targetFileName); + if (!(flags & SkipUpdateFile) && !existingTargetFile.remove()) { + *errorMessage = QString::fromLatin1("Cannot remove existing symbolic link %1: %2") + .arg(QDir::toNativeSeparators(targetFileName), existingTargetFile.errorString()); + return false; + } + } // target symbolic link exists + return createSymbolicLink(QFileInfo(targetDirectory + QLatin1Char('/') + relativeSource), sourceFileInfo.fileName(), errorMessage); + } // Source is symbolic link + + if (sourceFileInfo.isDir()) { + if ((flags & SkipQmlDesignerSpecificsDirectories) && sourceFileInfo.fileName() == QLatin1String("designer")) { + if (optVerboseLevel) + std::wcout << "Skipping " << QDir::toNativeSeparators(sourceFileName) << ".\n"; + return true; + } + bool created = false; + if (targetFileInfo.exists()) { + if (!targetFileInfo.isDir()) { + *errorMessage = QString::fromLatin1("%1 already exists and is not a directory.") + .arg(QDir::toNativeSeparators(targetFileName)); + return false; + } // Not a directory. + } else { // exists. + QDir d(targetDirectory); + if (optVerboseLevel) + std::wcout << "Creating " << targetFileName << ".\n"; + if (!(flags & SkipUpdateFile)) { + created = d.mkdir(sourceFileInfo.fileName()); + if (!created) { + *errorMessage = QString::fromLatin1("Cannot create directory %1 under %2.") + .arg(sourceFileInfo.fileName(), QDir::toNativeSeparators(targetDirectory)); + return false; + } + } + } + // Recurse into directory + QDir dir(sourceFileName); + + const QStringList allEntries = directoryFileEntryFunction(dir) + dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + for (const QString &entry : allEntries) + if (!updateFile(sourceFileName + QLatin1Char('/') + entry, directoryFileEntryFunction, targetFileName, flags, json, errorMessage)) + return false; + // Remove empty directories, for example QML import folders for which the filter did not match. + if (created && (flags & RemoveEmptyQmlDirectories)) { + QDir d(targetFileName); + const QStringList entries = d.entryList(QStringList(), QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); + if (entries.isEmpty() || (entries.size() == 1 && entries.first() == QLatin1String("qmldir"))) { + if (!d.removeRecursively()) { + *errorMessage = QString::fromLatin1("Cannot remove empty directory %1.") + .arg(QDir::toNativeSeparators(targetFileName)); + return false; + } + if (json) + json->removeTargetDirectory(targetFileName); + } + } + return true; + } // Source is directory. + + if (targetFileInfo.exists()) { + if (!(flags & ForceUpdateFile) + && targetFileInfo.lastModified() >= sourceFileInfo.lastModified()) { + if (optVerboseLevel) + std::wcout << sourceFileInfo.fileName() << " is up to date.\n"; + if (json) + json->addFile(sourceFileName, targetDirectory); + return true; + } + QFile targetFile(targetFileName); + if (!(flags & SkipUpdateFile) && !targetFile.remove()) { + *errorMessage = QString::fromLatin1("Cannot remove existing file %1: %2") + .arg(QDir::toNativeSeparators(targetFileName), targetFile.errorString()); + return false; + } + } // target exists + QFile file(sourceFileName); + if (optVerboseLevel) + std::wcout << "Updating " << sourceFileInfo.fileName() << ".\n"; + if (!(flags & SkipUpdateFile)) { + if (!file.copy(targetFileName)) { + *errorMessage = QString::fromLatin1("Cannot copy %1 to %2: %3") + .arg(QDir::toNativeSeparators(sourceFileName), + QDir::toNativeSeparators(targetFileName), + file.errorString()); + return false; + } + if (!(file.permissions() & QFile::WriteUser)) { // QTBUG-40152, clear inherited read-only attribute + QFile targetFile(targetFileName); + if (!targetFile.setPermissions(targetFile.permissions() | QFile::WriteUser)) { + *errorMessage = QString::fromLatin1("Cannot set write permission on %1: %2") + .arg(QDir::toNativeSeparators(targetFileName), file.errorString()); + return false; + } + } // Check permissions + } // !SkipUpdateFile + if (json) + json->addFile(sourceFileName, targetDirectory); + return true; +} + +// Base class to filter files by name filters functions to be passed to updateFile(). +class NameFilterFileEntryFunction { +public: + explicit NameFilterFileEntryFunction(const QStringList &nameFilters) : m_nameFilters(nameFilters) {} + QStringList operator()(const QDir &dir) const { return dir.entryList(m_nameFilters, QDir::Files); } + +private: + const QStringList m_nameFilters; +}; + +// Convenience for all files. +inline bool updateFile(const QString &sourceFileName, const QString &targetDirectory, unsigned flags, JsonOutput *json, QString *errorMessage) +{ + return updateFile(sourceFileName, NameFilterFileEntryFunction(QStringList()), targetDirectory, flags, json, errorMessage); +} + +QT_END_NAMESPACE + +#endif // UTILS_H diff --git a/tests/auto/tools/CMakeLists.txt b/tests/auto/tools/CMakeLists.txt index f1a9ced60e..8a827d11e4 100644 --- a/tests/auto/tools/CMakeLists.txt +++ b/tests/auto/tools/CMakeLists.txt @@ -15,3 +15,12 @@ if(TARGET Qt::DBus) add_subdirectory(qdbuscpp2xml) add_subdirectory(qdbusxml2cpp) endif() +if(QT_FEATURE_process AND NOT CMAKE_CROSSCOMPILING) + if(QT_FEATURE_macdeployqt) + add_subdirectory(macdeployqt) + endif() + if(QT_FEATURE_windeployqt AND BUILD_SHARED_LIBS) + # windeployqt does not work with static Qt builds. See QTBUG-69427. + add_subdirectory(windeployqt) + endif() +endif() diff --git a/tests/auto/tools/macdeployqt/CMakeLists.txt b/tests/auto/tools/macdeployqt/CMakeLists.txt new file mode 100644 index 0000000000..073c2a9e70 --- /dev/null +++ b/tests/auto/tools/macdeployqt/CMakeLists.txt @@ -0,0 +1,10 @@ +# Generated from macdeployqt.pro. + +##################################################################### +## tst_macdeployqt Test: +##################################################################### + +qt_internal_add_test(tst_macdeployqt + SOURCES + tst_macdeployqt.cpp +) diff --git a/tests/auto/tools/macdeployqt/source_basicapp/basicapp.pro b/tests/auto/tools/macdeployqt/source_basicapp/basicapp.pro new file mode 100644 index 0000000000..bba41b9c12 --- /dev/null +++ b/tests/auto/tools/macdeployqt/source_basicapp/basicapp.pro @@ -0,0 +1 @@ +SOURCES = main.cpp diff --git a/tests/auto/tools/macdeployqt/source_basicapp/main.cpp b/tests/auto/tools/macdeployqt/source_basicapp/main.cpp new file mode 100644 index 0000000000..093a882f32 --- /dev/null +++ b/tests/auto/tools/macdeployqt/source_basicapp/main.cpp @@ -0,0 +1,44 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite 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 <QGuiApplication> +#include <QRasterWindow> +#include <QScreen> +#include <QTimer> + +// Simple test application just to verify that it comes up properly + +int main(int argc, char ** argv) +{ + QGuiApplication app(argc, argv); + QRasterWindow w; + w.setTitle("macdeployqt test application"); + w.show(); + QTimer::singleShot(200, &w, &QCoreApplication::quit); + return app.exec(); +} diff --git a/tests/auto/tools/macdeployqt/source_plugin_sqlite/main.cpp b/tests/auto/tools/macdeployqt/source_plugin_sqlite/main.cpp new file mode 100644 index 0000000000..31e2e8117c --- /dev/null +++ b/tests/auto/tools/macdeployqt/source_plugin_sqlite/main.cpp @@ -0,0 +1,36 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite 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 <QtSql> + +int main(int argc, char ** argv) +{ + QCoreApplication app(argc, argv); + QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); + return db.isValid() ? 0 : 1; +} diff --git a/tests/auto/tools/macdeployqt/source_plugin_sqlite/plugin_sqlite.pro b/tests/auto/tools/macdeployqt/source_plugin_sqlite/plugin_sqlite.pro new file mode 100644 index 0000000000..e8183d3cee --- /dev/null +++ b/tests/auto/tools/macdeployqt/source_plugin_sqlite/plugin_sqlite.pro @@ -0,0 +1,2 @@ +SOURCES = main.cpp +QT += sql diff --git a/tests/auto/tools/macdeployqt/source_plugin_tls/main.cpp b/tests/auto/tools/macdeployqt/source_plugin_tls/main.cpp new file mode 100644 index 0000000000..7d1070b2c5 --- /dev/null +++ b/tests/auto/tools/macdeployqt/source_plugin_tls/main.cpp @@ -0,0 +1,35 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite 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 <QtNetwork> + +int main(int argc, char ** argv) +{ + QCoreApplication app(argc, argv); + return QSslSocket::supportsSsl() ? 0 : 1; +} diff --git a/tests/auto/tools/macdeployqt/source_plugin_tls/plugin_tls.pro b/tests/auto/tools/macdeployqt/source_plugin_tls/plugin_tls.pro new file mode 100644 index 0000000000..23954f5941 --- /dev/null +++ b/tests/auto/tools/macdeployqt/source_plugin_tls/plugin_tls.pro @@ -0,0 +1,2 @@ +SOURCES = main.cpp +QT += network diff --git a/tests/auto/tools/macdeployqt/tst_macdeployqt.cpp b/tests/auto/tools/macdeployqt/tst_macdeployqt.cpp new file mode 100644 index 0000000000..8922025570 --- /dev/null +++ b/tests/auto/tools/macdeployqt/tst_macdeployqt.cpp @@ -0,0 +1,316 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite 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 <QtCore> +#include <QtTest> + +bool g_testDirectoryBuild = false; // toggle to keep build output for debugging. +QTemporaryDir *g_temporaryDirectory; +QString g_macdeployqtBinary; +QString g_qmakeBinary; +QString g_makeBinary; +QString g_installNameToolBinary; + +#if QT_CONFIG(process) + +static const QString msgProcessError(const QProcess &process, const QString &what) +{ + QString result; + QTextStream(&result) << what << ": \"" << process.program() << ' ' + << process.arguments().join(QLatin1Char(' ')) << "\": " << process.errorString(); + return result; +} + +static bool runProcess(const QString &binary, + const QStringList &arguments, + QString *errorMessage, + const QString &workingDir = QString(), + const QProcessEnvironment &env = QProcessEnvironment(), + int timeOut = 10000, + QByteArray *stdOut = nullptr, QByteArray *stdErr = nullptr) +{ + QProcess process; + if (!env.isEmpty()) + process.setProcessEnvironment(env); + if (!workingDir.isEmpty()) + process.setWorkingDirectory(workingDir); + process.start(binary, arguments, QIODevice::ReadOnly); + if (!process.waitForStarted()) { + *errorMessage = msgProcessError(process, "Failed to start"); + return false; + } + if (!process.waitForFinished(timeOut)) { + *errorMessage = msgProcessError(process, "Timed out"); + process.terminate(); + if (!process.waitForFinished(300)) + process.kill(); + return false; + } + if (stdOut) + *stdOut = process.readAllStandardOutput(); + if (stdErr) + *stdErr= process.readAllStandardError(); + if (process.exitStatus() != QProcess::NormalExit) { + *errorMessage = msgProcessError(process, "Crashed"); + return false; + } + if (process.exitCode() != QProcess::NormalExit) { + *errorMessage = msgProcessError(process, "Exit code " + QString::number(process.exitCode())); + return false; + } + return true; +} + +#else + +static bool runProcess(const QString &binary, + const QStringList &arguments, + QString *arguments, + const QString &workingDir = QString(), + const QProcessEnvironment &env = QProcessEnvironment(), + int timeOut = 5000, + QByteArray *stdOut = Q_NULLPTR, QByteArray *stdErr = Q_NULLPTR) +{ + Q_UNUSED(binary); + Q_UNUSED(arguments); + Q_UNUSED(arguments); + Q_UNUSED(workingDir); + Q_UNUSED(env); + Q_UNUSED(timeOut); + Q_UNUSED(stdOut); + Q_UNUSED(stdErr); + return false; +} + +#endif + +QString sourcePath(const QString &name) +{ + return "source_" + name; +} + +QString buildPath(const QString &name) +{ + if (g_testDirectoryBuild) + return "build_" + name; + return g_temporaryDirectory->path() + "/build_" + name; +} + +bool qmake(const QString &source, const QString &destination, QString *errorMessage) +{ + QStringList args = QStringList() << source; + return runProcess(g_qmakeBinary, args, errorMessage, destination); +} + +bool make(const QString &destination, QString *errorMessage) +{ + QStringList args; + return runProcess(g_makeBinary, args, errorMessage, destination, + {}, 60000); +} + +void build(const QString &name) +{ + // Build the app or framework according to the convention used + // by this test: + // source_name (source code) + // build_name (build artifacts) + + QString source = sourcePath(name); + QString build = buildPath(name); + QString profile = name + ".pro"; + + QString sourcePath = QFINDTESTDATA(source); + QVERIFY(!sourcePath.isEmpty()); + + // Clear/set up build dir + QString buildPath = build; + QVERIFY(QDir(buildPath).removeRecursively()); + QVERIFY(QDir().mkdir(buildPath)); + QVERIFY(QDir(buildPath).exists()); + + // Build application + QString sourceProFile = QDir(sourcePath).canonicalPath() + '/' + profile; + QString errorMessage; + QVERIFY2(qmake(sourceProFile, buildPath, &errorMessage), qPrintable(errorMessage)); + QVERIFY2(make(buildPath, &errorMessage), qPrintable(errorMessage)); +} + +bool changeInstallName(const QString &path, const QString &binary, const QString &from, const QString &to) +{ + QStringList args = QStringList() << binary << "-change" << from << to; + QString errorMessage; + return runProcess(g_installNameToolBinary, args, &errorMessage, path); +} + +bool deploy(const QString &name, const QStringList &options, QString *errorMessage) +{ + QString bundle = name + ".app"; + QString path = buildPath(name); + QStringList args = QStringList() << bundle << options; + return runProcess(g_macdeployqtBinary, args, errorMessage, path); +} + +bool debugDeploy(const QString &name, const QStringList &options, QString *errorMessage) +{ + QString bundle = name + ".app"; + QString path = buildPath(name); + QStringList args = QStringList() << bundle << options << "-verbose=3"; + QByteArray stdOut; + QByteArray stdErr; + bool exitOK = runProcess(g_macdeployqtBinary, args, errorMessage, path, QProcessEnvironment(), + 10000, &stdOut, &stdErr); + + qDebug() << "macdeployqt exit OK" << exitOK; + qDebug() << qPrintable(stdOut); + qDebug() << qPrintable(stdErr); + + return exitOK; +} + +bool run(const QString &name, QString *errorMessage) +{ + QString path = buildPath(name); + QStringList args; + QString binary = name + ".app/Contents/MacOS/" + name; + return runProcess(binary, args, errorMessage, path); +} + +bool runPrintLibraries(const QString &name, QString *errorMessage, QByteArray *stdErr) +{ + QString binary = name + ".app/Contents/MacOS/" + name; + QString path = buildPath(name); + QStringList args; + QProcessEnvironment env = QProcessEnvironment(); + env.insert("DYLD_PRINT_LIBRARIES", "true"); + QByteArray stdOut; + return runProcess(binary, args, errorMessage, path, env, 5000, &stdOut, stdErr); +} + +void runVerifyDeployment(const QString &name) +{ + QString errorMessage; + // Verify that the application runs after deployment and that it loads binaries from + // the application bundle only. + QByteArray libraries; + QVERIFY2(runPrintLibraries(name, &errorMessage, &libraries), qPrintable(errorMessage)); + const QList<QString> parts = QString::fromLocal8Bit(libraries).split("dyld: loaded:"); + const QString qtPath = QLibraryInfo::path(QLibraryInfo::PrefixPath); + // Let assume Qt is not installed in system + foreach (QString part, parts) { + part = part.trimmed(); + if (part.isEmpty()) + continue; + QVERIFY(!parts.startsWith(qtPath)); + } +} + +class tst_macdeployqt : public QObject +{ + Q_OBJECT +private slots: + void initTestCase(); + void cleanupTestCase(); + void basicapp(); + void plugins_data(); + void plugins(); +}; + +void tst_macdeployqt::initTestCase() +{ +#ifdef QT_NO_PROCESS + QSKIP("This test requires QProcess support"); +#endif + + // Set up test-global unique temporary directory + g_temporaryDirectory = new QTemporaryDir(); + QVERIFY(g_temporaryDirectory->isValid()); + + // Locate build and deployment tools + g_macdeployqtBinary = QLibraryInfo::path(QLibraryInfo::BinariesPath) + "/macdeployqt"; + QVERIFY(!g_macdeployqtBinary.isEmpty()); + g_qmakeBinary = QLibraryInfo::path(QLibraryInfo::BinariesPath) + "/qmake"; + QVERIFY(!g_qmakeBinary.isEmpty()); + g_makeBinary = QStandardPaths::findExecutable("make"); + QVERIFY(!g_makeBinary.isEmpty()); + g_installNameToolBinary = QStandardPaths::findExecutable("install_name_tool"); + QVERIFY(!g_installNameToolBinary.isEmpty()); +} + +void tst_macdeployqt::cleanupTestCase() +{ + delete g_temporaryDirectory; +} + +// Verify that deployment of a basic Qt Gui application works +void tst_macdeployqt::basicapp() +{ +#ifdef QT_NO_PROCESS + QSKIP("This test requires QProcess support"); +#endif + + QString errorMessage; + QString name = "basicapp"; + + // Build and verify that the application runs before deployment + build(name); + QVERIFY2(run(name, &errorMessage), qPrintable(errorMessage)); + + // Deploy application + QVERIFY2(deploy(name, QStringList(), &errorMessage), qPrintable(errorMessage)); + + // Verify deployment + runVerifyDeployment(name); +} + +void tst_macdeployqt::plugins_data() +{ + QTest::addColumn<QString>("name"); + QTest::newRow("sqlite") << "plugin_sqlite"; + QTest::newRow("tls") << "plugin_tls"; +} + +void tst_macdeployqt::plugins() +{ + QFETCH(QString, name); + + build(name); + + // Verify that the test app runs before deployment. + QString errorMessage; + if (!run(name, &errorMessage)) { + qDebug() << qPrintable(errorMessage); + QSKIP("Could not run test application before deployment"); + } + + QVERIFY2(deploy(name, QStringList(), &errorMessage), qPrintable(errorMessage)); + runVerifyDeployment(name); +} + +QTEST_MAIN(tst_macdeployqt) +#include "tst_macdeployqt.moc" diff --git a/tests/auto/tools/windeployqt/CMakeLists.txt b/tests/auto/tools/windeployqt/CMakeLists.txt new file mode 100644 index 0000000000..5eae1ca164 --- /dev/null +++ b/tests/auto/tools/windeployqt/CMakeLists.txt @@ -0,0 +1,4 @@ +# Generated from windeployqt.pro. + +add_subdirectory(testapp) +add_subdirectory(test) diff --git a/tests/auto/tools/windeployqt/test/CMakeLists.txt b/tests/auto/tools/windeployqt/test/CMakeLists.txt new file mode 100644 index 0000000000..d412249a7f --- /dev/null +++ b/tests/auto/tools/windeployqt/test/CMakeLists.txt @@ -0,0 +1,11 @@ +# Generated from test.pro. + +##################################################################### +## tst_windeployqt Test: +##################################################################### + +qt_internal_add_test(tst_windeployqt + OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/../" # special case + SOURCES + ../tst_windeployqt.cpp +) diff --git a/tests/auto/tools/windeployqt/testapp/CMakeLists.txt b/tests/auto/tools/windeployqt/testapp/CMakeLists.txt new file mode 100644 index 0000000000..83851dae65 --- /dev/null +++ b/tests/auto/tools/windeployqt/testapp/CMakeLists.txt @@ -0,0 +1,21 @@ +# Generated from testapp.pro. + +##################################################################### +## testapp Binary: +##################################################################### + +qt_internal_add_executable(windeploy_testapp # special case + GUI + OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/" + SOURCES + main.cpp + PUBLIC_LIBRARIES + Qt::Gui +) + +# special case begin +set_target_properties(windeploy_testapp + PROPERTIES + OUTPUT_NAME testapp +) +# special case end diff --git a/tests/auto/tools/windeployqt/testapp/main.cpp b/tests/auto/tools/windeployqt/testapp/main.cpp new file mode 100644 index 0000000000..4dc03cdad4 --- /dev/null +++ b/tests/auto/tools/windeployqt/testapp/main.cpp @@ -0,0 +1,46 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite 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 <QGuiApplication> +#include <QRasterWindow> +#include <QScreen> +#include <QTimer> + +// Simple test application just to verify that it comes up properly + +int main(int argc, char ** argv) +{ + QGuiApplication app(argc, argv); + QRasterWindow w; + w.setTitle("windeployqt test application"); + const QRect availableGeometry = QGuiApplication::primaryScreen()->availableGeometry(); + w.resize(availableGeometry.size() / 4); + w.show(); + QTimer::singleShot(200, &w, &QCoreApplication::quit); + return app.exec(); +} diff --git a/tests/auto/tools/windeployqt/tst_windeployqt.cpp b/tests/auto/tools/windeployqt/tst_windeployqt.cpp new file mode 100644 index 0000000000..38403c90fc --- /dev/null +++ b/tests/auto/tools/windeployqt/tst_windeployqt.cpp @@ -0,0 +1,181 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite 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 <QtCore/QDebug> +#include <QtCore/QDir> +#include <QtCore/QFile> +#include <QtCore/QFileInfo> +#include <QtCore/QLibraryInfo> +#include <QtCore/QProcess> +#include <QtCore/QProcessEnvironment> +#include <QtCore/QStandardPaths> +#include <QtCore/QTextStream> +#include <QtTest/QtTest> + +static const QString msgProcessError(const QProcess &process, const QString &what, + const QByteArray &stdOut = QByteArray(), + const QByteArray &stdErr = QByteArray()) +{ + QString result; + QTextStream str(&result); + str << what << ": \"" << process.program() << ' ' + << process.arguments().join(QLatin1Char(' ')) << "\": " << process.errorString(); + if (!stdOut.isEmpty()) + str << "\nStandard output:\n" << stdOut; + if (!stdErr.isEmpty()) + str << "\nStandard error:\n" << stdErr; + return result; +} + +static bool runProcess(const QString &binary, + const QStringList &arguments, + QString *errorMessage, + const QString &workingDir = QString(), + const QProcessEnvironment &env = QProcessEnvironment(), + int timeOut = 5000, + QByteArray *stdOutIn = nullptr, QByteArray *stdErrIn = nullptr) +{ + QProcess process; + if (!env.isEmpty()) + process.setProcessEnvironment(env); + if (!workingDir.isEmpty()) + process.setWorkingDirectory(workingDir); + qDebug().noquote().nospace() << "Running: " << QDir::toNativeSeparators(binary) + << ' ' << arguments.join(QLatin1Char(' ')); + process.start(binary, arguments, QIODevice::ReadOnly); + if (!process.waitForStarted()) { + *errorMessage = msgProcessError(process, "Failed to start"); + return false; + } + if (!process.waitForFinished(timeOut)) { + *errorMessage = msgProcessError(process, "Timed out"); + process.terminate(); + if (!process.waitForFinished(300)) + process.kill(); + return false; + } + const QByteArray stdOut = process.readAllStandardOutput(); + const QByteArray stdErr = process.readAllStandardError(); + if (stdOutIn) + *stdOutIn = stdOut; + if (stdErrIn) + *stdErrIn = stdErr; + if (process.exitStatus() != QProcess::NormalExit) { + *errorMessage = msgProcessError(process, "Crashed", stdOut, stdErr); + return false; + } + if (process.exitCode() != QProcess::NormalExit) { + *errorMessage = msgProcessError(process, "Exit code " + QString::number(process.exitCode()), + stdOut, stdErr); + return false; + } + return true; +} + +class tst_windeployqt : public QObject +{ + Q_OBJECT +private slots: + void initTestCase(); + void help(); + void deploy(); + +private: + QString m_windeployqtBinary; + QString m_testApp; + QString m_testAppBinary; +}; + +void tst_windeployqt::initTestCase() +{ + m_windeployqtBinary = QStandardPaths::findExecutable("windeployqt"); + QVERIFY(!m_windeployqtBinary.isEmpty()); + m_testApp = QFINDTESTDATA("testapp"); + QVERIFY(!m_testApp.isEmpty()); + const QFileInfo testAppBinary(m_testApp + QLatin1String("/testapp.exe")); + QVERIFY2(testAppBinary.isFile(), qPrintable(testAppBinary.absoluteFilePath())); + m_testAppBinary = testAppBinary.absoluteFilePath(); +} + +void tst_windeployqt::help() +{ + QString errorMessage; + QByteArray stdOut; + QByteArray stdErr; + QVERIFY2(runProcess(m_windeployqtBinary, QStringList("--help"), &errorMessage, + QString(), QProcessEnvironment(), 5000, &stdOut, &stdErr), + qPrintable(errorMessage)); + QVERIFY2(!stdOut.isEmpty(), stdErr); +} + +// deploy(): Deploys the test application and launches it with Qt removed from the environment +// to verify it runs stand-alone. + +void tst_windeployqt::deploy() +{ + QString errorMessage; + // Deploy application + QStringList deployArguments; + deployArguments << QLatin1String("--no-translations") << QDir::toNativeSeparators(m_testAppBinary); + QVERIFY2(runProcess(m_windeployqtBinary, deployArguments, &errorMessage, QString(), QProcessEnvironment(), 20000), + qPrintable(errorMessage)); + + // Create environment with Qt and all "lib" paths removed. + const QString qtBinDir = QDir::toNativeSeparators(QLibraryInfo::path(QLibraryInfo::BinariesPath)); + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + const QString pathKey = QLatin1String("PATH"); + const QChar pathSeparator(QLatin1Char(';')); // ### fixme: Qt 5.6: QDir::listSeparator() + const QString origPath = env.value(pathKey); + QString newPath; + const QStringList pathElements = origPath.split(pathSeparator, Qt::SkipEmptyParts); + for (const QString &pathElement : pathElements) { + if (pathElement.compare(qtBinDir, Qt::CaseInsensitive) + && !pathElement.contains(QLatin1String("\\lib"), Qt::CaseInsensitive)) { + if (!newPath.isEmpty()) + newPath.append(pathSeparator); + newPath.append(pathElement); + } + } + if (newPath == origPath) + qWarning() << "Unable to remove Qt from PATH"; + env.insert(pathKey, newPath); + + // Create qt.conf to enforce usage of local plugins + QFile qtConf(QFileInfo(m_testAppBinary).absolutePath() + QLatin1String("/qt.conf")); + QVERIFY2(qtConf.open(QIODevice::WriteOnly | QIODevice::Text), + qPrintable(qtConf.fileName() + QLatin1String(": ") + qtConf.errorString())); + QVERIFY(qtConf.write("[Paths]\nPrefix = .\n")); + qtConf.close(); + + // Verify that application still runs + QVERIFY2(runProcess(m_testAppBinary, QStringList(), &errorMessage, QString(), env, 10000), + qPrintable(errorMessage)); +} + +QTEST_MAIN(tst_windeployqt) +#include "tst_windeployqt.moc" |