diff options
Diffstat (limited to 'src/tools/windeployqt')
-rw-r--r-- | src/tools/windeployqt/CMakeLists.txt | 33 | ||||
-rw-r--r-- | src/tools/windeployqt/main.cpp | 2007 | ||||
-rw-r--r-- | src/tools/windeployqt/qmlutils.cpp | 138 | ||||
-rw-r--r-- | src/tools/windeployqt/qmlutils.h | 40 | ||||
-rw-r--r-- | src/tools/windeployqt/qtmoduleinfo.cpp | 183 | ||||
-rw-r--r-- | src/tools/windeployqt/qtmoduleinfo.h | 51 | ||||
-rw-r--r-- | src/tools/windeployqt/qtplugininfo.cpp | 100 | ||||
-rw-r--r-- | src/tools/windeployqt/qtplugininfo.h | 48 | ||||
-rw-r--r-- | src/tools/windeployqt/utils.cpp | 1022 | ||||
-rw-r--r-- | src/tools/windeployqt/utils.h | 366 |
10 files changed, 3988 insertions, 0 deletions
diff --git a/src/tools/windeployqt/CMakeLists.txt b/src/tools/windeployqt/CMakeLists.txt new file mode 100644 index 0000000000..2e50116484 --- /dev/null +++ b/src/tools/windeployqt/CMakeLists.txt @@ -0,0 +1,33 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +##################################################################### +## windeployqt Tool: +##################################################################### + +qt_get_tool_target_name(target_name windeployqt) +qt_internal_add_tool(${target_name} + TOOLS_TARGET Core + USER_FACING + INSTALL_VERSIONED_LINK + TARGET_DESCRIPTION "Qt Windows Deployment Tool" + SOURCES + qmlutils.cpp qmlutils.h + qtmoduleinfo.cpp qtmoduleinfo.h + qtplugininfo.cpp qtplugininfo.h + utils.cpp utils.h + main.cpp + DEFINES + QT_NO_CAST_FROM_ASCII + QT_NO_CAST_TO_ASCII + QT_NO_FOREACH + QT_NO_QPAIR + LIBRARIES + Qt::CorePrivate +) +qt_internal_return_unless_building_tools() + +qt_internal_extend_target(${target_name} CONDITION WIN32 + PUBLIC_LIBRARIES + shlwapi +) diff --git a/src/tools/windeployqt/main.cpp b/src/tools/windeployqt/main.cpp new file mode 100644 index 0000000000..084345a4d8 --- /dev/null +++ b/src/tools/windeployqt/main.cpp @@ -0,0 +1,2007 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "utils.h" +#include "qmlutils.h" +#include "qtmoduleinfo.h" +#include "qtplugininfo.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 <QtCore/private/qconfig_p.h> + +#include <algorithm> +#include <cstdio> +#include <iostream> +#include <iterator> +#include <unordered_map> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +static QtModuleInfoStore qtModuleEntries; + +#define DECLARE_KNOWN_MODULE(name) \ + static size_t Qt##name ## ModuleId = QtModule::InvalidId + +DECLARE_KNOWN_MODULE(3DQuick); +DECLARE_KNOWN_MODULE(Core); +DECLARE_KNOWN_MODULE(Designer); +DECLARE_KNOWN_MODULE(DesignerComponents); +DECLARE_KNOWN_MODULE(Gui); +DECLARE_KNOWN_MODULE(Qml); +DECLARE_KNOWN_MODULE(QmlTooling); +DECLARE_KNOWN_MODULE(Quick); +DECLARE_KNOWN_MODULE(WebEngineCore); +DECLARE_KNOWN_MODULE(Widgets); + +#define DEFINE_KNOWN_MODULE(name) \ + m[QLatin1String("Qt6" #name)] = &Qt##name ## ModuleId + +static void assignKnownModuleIds() +{ + std::unordered_map<QString, size_t *> m; + DEFINE_KNOWN_MODULE(3DQuick); + DEFINE_KNOWN_MODULE(Core); + DEFINE_KNOWN_MODULE(Designer); + DEFINE_KNOWN_MODULE(DesignerComponents); + DEFINE_KNOWN_MODULE(Gui); + DEFINE_KNOWN_MODULE(Qml); + DEFINE_KNOWN_MODULE(QmlTooling); + DEFINE_KNOWN_MODULE(Quick); + DEFINE_KNOWN_MODULE(WebEngineCore); + DEFINE_KNOWN_MODULE(Widgets); + for (size_t i = 0; i < qtModuleEntries.size(); ++i) { + const QtModule &module = qtModuleEntries.moduleById(i); + auto it = m.find(module.name); + if (it == m.end()) + continue; + *(it->second) = i; + } +} + +#undef DECLARE_KNOWN_MODULE +#undef DEFINE_KNOWN_MODULE + +static const char webEngineProcessC[] = "QtWebEngineProcess"; + +static inline QString webProcessBinary(const char *binaryName, Platform p) +{ + const QString webProcess = QLatin1StringView(binaryName); + return (p & WindowsBased) ? webProcess + QStringLiteral(".exe") : webProcess; +} + +static QString moduleNameToOptionName(const QString &moduleName) +{ + QString result = moduleName + .mid(3) // strip the "Qt6" prefix + .toLower(); + if (result == u"help"_s) + result.prepend("qt"_L1); + return result; +} + +static QByteArray formatQtModules(const ModuleBitset &mask, bool option = false) +{ + QByteArray result; + for (const auto &qtModule : qtModuleEntries) { + if (mask.test(qtModule.id)) { + if (!result.isEmpty()) + result.append(' '); + result.append(option + ? moduleNameToOptionName(qtModule.name).toUtf8() + : qtModule.name.toUtf8()); + if (qtModule.internal) + result.append("Internal"); + } + } + return result; +} + +static QString formatQtPlugins(const PluginInformation &pluginInfo) +{ + QString result(u'\n'); + for (const auto &pair : pluginInfo.typeMap()) { + result += pair.first; + result += u": \n"; + for (const QString &plugin : pair.second) { + result += u" "; + result += plugin; + result += u'\n'; + } + } + return result; +} + +static Platform platformFromMkSpec(const QString &xSpec) +{ + if (xSpec.startsWith("win32-"_L1)) { + if (xSpec.contains("clang-g++"_L1)) + return WindowsDesktopClangMinGW; + if (xSpec.contains("clang-msvc++"_L1)) + return WindowsDesktopClangMsvc; + if (xSpec.contains("arm"_L1)) + return WindowsDesktopMsvcArm; + + return xSpec.contains("g++"_L1) ? WindowsDesktopMinGW : WindowsDesktopMsvcIntel; + } + 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 systemDxc = true; + bool compilerRunTime = false; + bool softwareRasterizer = true; + bool ffmpeg = true; + PluginSelections pluginSelections; + Platform platform = WindowsDesktopMsvcIntel; + ModuleBitset additionalLibraries; + ModuleBitset disabledLibraries; + 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; + QString openSslRootDirectory; + QString qmlDirectory; + QStringList binaries; + JsonOutput *json = nullptr; + ListOption list = ListNone; + DebugDetection debugDetection = DebugDetectionAuto; + bool deployPdb = false; + bool dryRun = false; + bool patchQt = true; + bool ignoreLibraryErrors = false; + bool deployInsightTrackerPlugin = false; + bool forceOpenSslPlugin = 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(QLatin1StringView(webEngineProcessC), Qt::CaseInsensitive)) { + return binaryFi.absoluteFilePath(); + } + } + return QString(); +} + +static QString msgFileDoesNotExist(const QString & file) +{ + return u'"' + QDir::toNativeSeparators(file) + "\" does not exist."_L1; +} + +enum CommandLineParseFlag { + CommandLineParseError = 0x1, + CommandLineParseHelpRequested = 0x2, + CommandLineVersionRequested = 0x4 +}; + +static QCommandLineOption createQMakeOption() +{ + return { + u"qmake"_s, + u"Use specified qmake instead of qmake from PATH. Deprecated, use qtpaths instead."_s, + u"path"_s + }; +} + +static QCommandLineOption createQtPathsOption() +{ + return { + u"qtpaths"_s, + u"Use specified qtpaths.exe instead of qtpaths.exe from PATH."_s, + u"path"_s + }; +} + +static QCommandLineOption createVerboseOption() +{ + return { + u"verbose"_s, + u"Verbose level (0-2)."_s, + u"level"_s + }; +} + +static int parseEarlyArguments(const QStringList &arguments, Options *options, + QString *errorMessage) +{ + QCommandLineParser parser; + parser.setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions); + + QCommandLineOption qmakeOption = createQMakeOption(); + parser.addOption(qmakeOption); + + QCommandLineOption qtpathsOption = createQtPathsOption(); + parser.addOption(qtpathsOption); + + QCommandLineOption verboseOption = createVerboseOption(); + parser.addOption(verboseOption); + + // Deliberately don't check for errors. We want to ignore options we don't know about. + parser.parse(arguments); + + 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 -qtpaths 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 = u'"' + QDir::toNativeSeparators(qtpathsBinary) + + QStringLiteral("\" is not an executable."); + return CommandLineParseError; + } + options->qtpathsBinary = qtpathsBinary; + } + + 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; + } + } + + return 0; +} + +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(u"Qt Deploy Tool " QT_VERSION_STR + "\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>"_s); + const QCommandLineOption helpOption = parser->addHelpOption(); + const QCommandLineOption versionOption = parser->addVersionOption(); + + QCommandLineOption dirOption(QStringLiteral("dir"), + QStringLiteral("Use directory instead of binary directory."), + QStringLiteral("directory")); + parser->addOption(dirOption); + + // Add early options to have them available in the help text. + parser->addOption(createQMakeOption()); + parser->addOption(createQtPathsOption()); + + 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); + + const QCommandLineOption translationDirOption( + u"translationdir"_s, + u"Copy translations to path."_s, + u"path"_s); + parser->addOption(translationDirOption); + + QCommandLineOption qmlDeployDirOption(QStringLiteral("qml-deploy-dir"), + QStringLiteral("Copy qml files to path."), + QStringLiteral("path")); + parser->addOption(qmlDeployDirOption); + + 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 includeSoftPluginsOption(QStringLiteral("include-soft-plugins"), + QStringLiteral("Include in the deployment all relevant plugins by taking into account all soft dependencies.")); + parser->addOption(includeSoftPluginsOption); + + QCommandLineOption skipPluginTypesOption(QStringLiteral("skip-plugin-types"), + QStringLiteral("A comma-separated list of plugin types that are not deployed (qmltooling,generic)."), + QStringLiteral("plugin types")); + parser->addOption(skipPluginTypesOption); + + QCommandLineOption addPluginTypesOption(QStringLiteral("add-plugin-types"), + QStringLiteral("A comma-separated list of plugin types that will be added to deployment (imageformats,iconengines)"), + QStringLiteral("plugin types")); + parser->addOption(addPluginTypesOption); + + QCommandLineOption includePluginsOption(QStringLiteral("include-plugins"), + QStringLiteral("A comma-separated list of individual plugins that will be added to deployment (scene2d,qjpeg)"), + QStringLiteral("plugins")); + parser->addOption(includePluginsOption); + + QCommandLineOption excludePluginsOption(QStringLiteral("exclude-plugins"), + QStringLiteral("A comma-separated list of individual plugins that will not be deployed (qsvg,qpdf)"), + QStringLiteral("plugins")); + parser->addOption(excludePluginsOption); + + 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 noSystemDxcOption(QStringLiteral("no-system-dxc-compiler"), + QStringLiteral("Skip deployment of the system DXC (dxcompiler.dll, dxil.dll).")); + parser->addOption(noSystemDxcOption); + + + QCommandLineOption compilerRunTimeOption(QStringLiteral("compiler-runtime"), + QStringLiteral("Deploy compiler runtime (Desktop only).")); + parser->addOption(compilerRunTimeOption); + + 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 noFFmpegOption(QStringLiteral("no-ffmpeg"), + QStringLiteral("Do not deploy the FFmpeg libraries.")); + parser->addOption(noFFmpegOption); + + QCommandLineOption forceOpenSslOption(QStringLiteral("force-openssl"), + QStringLiteral("Deploy openssl plugin but ignore openssl library dependency")); + parser->addOption(forceOpenSslOption); + + QCommandLineOption openSslRootOption(QStringLiteral("openssl-root"), + QStringLiteral("Directory containing openSSL libraries."), + QStringLiteral("directory")); + parser->addOption(openSslRootOption); + + + QCommandLineOption listOption(QStringLiteral("list"), + "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"_L1, + QStringLiteral("option")); + parser->addOption(listOption); + + // Add early option to have it available in the help text. + parser->addOption(createVerboseOption()); + + parser->addPositionalArgument(QStringLiteral("[files]"), + QStringLiteral("Binaries or directory containing the binary.")); + + QCommandLineOption deployInsightTrackerOption(QStringLiteral("deploy-insighttracker"), + QStringLiteral("Deploy insight tracker plugin.")); + // The option will be added to the parser if the module is available (see block below) + bool insightTrackerModuleAvailable = false; + + OptionPtrVector enabledModuleOptions; + OptionPtrVector disabledModuleOptions; + const size_t qtModulesCount = qtModuleEntries.size(); + enabledModuleOptions.reserve(qtModulesCount); + disabledModuleOptions.reserve(qtModulesCount); + for (const QtModule &module : qtModuleEntries) { + const QString option = moduleNameToOptionName(module.name); + const QString name = module.name; + if (name == u"InsightTracker") { + parser->addOption(deployInsightTrackerOption); + insightTrackerModuleAvailable = true; + } + 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 (parser->isSet(versionOption)) + return CommandLineVersionRequested; + if (!success) { + *errorMessage = parser->errorText(); + return CommandLineParseError; + } + + options->libraryDirectory = parser->value(libDirOption); + options->pluginDirectory = parser->value(pluginDirOption); + options->translationsDirectory = parser->value(translationDirOption); + options->qmlDirectory = parser->value(qmlDeployDirOption); + 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(u','); + options->systemD3dCompiler = !parser->isSet(noSystemD3DCompilerOption); + options->systemDxc = !parser->isSet(noSystemDxcOption); + options->quickImports = !parser->isSet(noQuickImportOption); + + // default to deployment of compiler runtime for windows desktop configurations + if (options->platform == WindowsDesktopMinGW || options->platform.testFlags(WindowsDesktopMsvc) + || parser->isSet(compilerRunTimeOption)) + options->compilerRunTime = true; + if (parser->isSet(noCompilerRunTimeOption)) + options->compilerRunTime = false; + + if (options->compilerRunTime && options->platform != WindowsDesktopMinGW + && !options->platform.testFlags(WindowsDesktopMsvc)) { + *errorMessage = QStringLiteral("Deployment of the compiler runtime is implemented for Desktop MSVC/g++ only."); + return CommandLineParseError; + } + + options->pluginSelections.includeSoftPlugins = parser->isSet(includeSoftPluginsOption); + + if (parser->isSet(skipPluginTypesOption)) + options->pluginSelections.disabledPluginTypes = parser->value(skipPluginTypesOption).split(u','); + + if (parser->isSet(addPluginTypesOption)) + options->pluginSelections.enabledPluginTypes = parser->value(addPluginTypesOption).split(u','); + + if (parser->isSet(includePluginsOption)) + options->pluginSelections.includedPlugins = parser->value(includePluginsOption).split(u','); + + if (parser->isSet(excludePluginsOption)) + options->pluginSelections.excludedPlugins = parser->value(excludePluginsOption).split(u','); + + 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(noFFmpegOption)) + options->ffmpeg = false; + + if (parser->isSet(forceOpenSslOption)) + options->forceOpenSslPlugin = true; + + if (parser->isSet(openSslRootOption)) + options->openSslRootDirectory = parser->value(openSslRootOption); + + if (options->forceOpenSslPlugin && !options->openSslRootDirectory.isEmpty()) { + *errorMessage = QStringLiteral("force-openssl and openssl-root are mutually exclusive"); + return CommandLineParseError; + } + + 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); + if (insightTrackerModuleAvailable) + options->deployInsightTrackerPlugin = parser->isSet(deployInsightTrackerOption); + + for (const QtModule &module : qtModuleEntries) { + if (parser->isSet(*enabledModuleOptions.at(module.id))) + options->additionalLibraries[module.id] = 1; + if (parser->isSet(*disabledModuleOptions.at(module.id))) + options->disabledLibraries[module.id] = 1; + } + + // Add some dependencies + if (options->additionalLibraries.test(QtQuickModuleId)) + options->additionalLibraries[QtQmlModuleId] = 1; + if (options->additionalLibraries.test(QtDesignerComponentsModuleId)) + options->additionalLibraries[QtDesignerModuleId] = 1; + + 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; + } + + 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(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 = u'"' + 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 + u'"'; + return CommandLineParseError; + } + options->directory = fi.absoluteFilePath(); + options->binaries.append(binary); + } // directory. + + // Remaining files or plugin directories + bool multipleDirs = false; + 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 + u'/' + library); + } else { + if (!parser->isSet(dirOption) && fi.absolutePath() != options->directory) + multipleDirs = true; + options->binaries.append(path); + } + } + if (multipleDirs) { + std::wcerr << "Warning: using binaries from different directories, deploying to following path: " + << options->directory << '\n' << "To disable this warning, use the --dir option\n"; + } + if (options->translationsDirectory.isEmpty()) + options->translationsDirectory = options->directory + "/translations"_L1; + return 0; +} + +// Simple line wrapping at 80 character boundaries. +static inline QString lineBreak(QString s) +{ + for (qsizetype i = 80; i < s.size(); i += 80) { + const qsizetype lastBlank = s.lastIndexOf(u' ', i); + if (lastBlank >= 0) { + s[lastBlank] = u'\n'; + i = lastBlank + 1; + } + } + return s; +} + +static inline QString helpText(const QCommandLineParser &p, const PluginInformation &pluginInfo) +{ + QString result = p.helpText(); + // Replace the default-generated text which is too long by a short summary + // explaining how to enable single libraries. + if (qtModuleEntries.size() == 0) + return result; + const QtModule &firstModule = qtModuleEntries.moduleById(0); + const QString firstModuleOption = moduleNameToOptionName(firstModule.name); + const qsizetype moduleStart = result.indexOf("\n --"_L1 + firstModuleOption); + const qsizetype argumentsStart = result.lastIndexOf("\nArguments:"_L1); + if (moduleStart >= argumentsStart) + return result; + QString moduleHelp; + moduleHelp += + "\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"_L1; + ModuleBitset mask; + moduleHelp += lineBreak(QString::fromLatin1(formatQtModules(mask.set(), true))); + moduleHelp += u"\n\n"; + moduleHelp += + u"Qt plugins can be included or excluded individually or by type.\n" + u"To deploy or block plugins individually, use the --include-plugins\n" + u"and --exclude-plugins options (--include-plugins qjpeg,qsvgicon)\n" + u"You can also use the --skip-plugin-types or --add-plugin-types to\n" + u"achieve similar results with entire plugin groups, like imageformats, e.g.\n" + u"(--add-plugin-types imageformats,iconengines). Exclusion always takes\n" + u"precedence over inclusion, and types take precedence over specific plugins.\n" + u"For example, including qjpeg, but skipping imageformats, will NOT deploy qjpeg.\n" + u"\nDetected available plugins:\n"; + moduleHelp += formatQtPlugins(pluginInfo); + 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("Qt"_L1, 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 (!readPeExecutable(binary, errorMessage, &dependentLibs, wordSize, isDebug, + platform == WindowsDesktopMinGW, machineArch)) { + errorMessage->prepend("Unable to find dependent libraries of "_L1 + + QDir::toNativeSeparators(binary) + " :"_L1); + 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 : std::as_const(dependentLibs)) { + if (isQtModule(lib)) { + const QString path = normalizeFileName(qtBinDir + u'/' + 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 qsizetype lastDot = libraryFileName.lastIndexOf(u'.') + 1; + if (lastDot <= 0) + return QString(); + libraryFileName.replace(lastDot, libraryFileName.size() - lastDot, "pdb"_L1); + 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 debug) and .qml/,js, etc. +class QmlDirectoryFileEntryFunction { +public: + enum Flags { + DeployPdb = 0x1, + SkipSources = 0x2 + }; + + explicit QmlDirectoryFileEntryFunction( + const QString &moduleSourcePath, Platform platform, DebugMatchMode debugMatchMode, unsigned flags) + : m_flags(flags), m_qmlNameFilter(QmlDirectoryFileEntryFunction::qmlNameFilters(flags)) + , m_dllFilter(platform, debugMatchMode), m_moduleSourcePath(moduleSourcePath) + {} + + QStringList operator()(const QDir &dir) const + { + if (moduleSourceDir(dir).canonicalPath() != m_moduleSourcePath) { + // If we're in a different module, return nothing. + return {}; + } + + 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 QDir moduleSourceDir(const QDir &dir) + { + QDir moduleSourceDir = dir; + while (!moduleSourceDir.exists(QStringLiteral("qmldir"))) { + if (!moduleSourceDir.cdUp()) { + return {}; + } + } + return moduleSourceDir; + } + + 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; + QString m_moduleSourcePath; +}; + +static qint64 qtModule(QString module, const QString &infix) +{ + // Match needle 'path/Qt6Core<infix><d>.dll' or 'path/libQt6Core<infix>.so.5.0' + const qsizetype lastSlashPos = module.lastIndexOf(u'/'); + if (lastSlashPos > 0) + module.remove(0, lastSlashPos + 1); + if (module.startsWith("lib"_L1)) + module.remove(0, 3); + int endPos = infix.isEmpty() ? -1 : module.lastIndexOf(infix); + if (endPos == -1) + endPos = module.indexOf(u'.'); // strip suffixes '.so.5.0'. + if (endPos > 0) + module.truncate(endPos); + // That should leave us with 'Qt6Core<d>'. + for (const auto &qtModule : qtModuleEntries) { + const QString &libraryName = qtModule.name; + if (module == libraryName + || (module.size() == libraryName.size() + 1 && module.startsWith(libraryName))) { + return qtModule.id; + } + } + std::wcerr << "Warning: module " << qPrintable(module) << " could not be found\n"; + return -1; +} + +// Return the path if a plugin is to be deployed +static QString deployPlugin(const QString &plugin, const QDir &subDir, const bool dueToModule, + const DebugMatchMode &debugMatchMode, ModuleBitset *pluginNeededQtModules, + const ModuleBitset &disabledQtModules, + const PluginSelections &pluginSelections, const QString &libraryLocation, + const QString &infix, Platform platform, + bool deployInsightTrackerPlugin, bool deployOpenSslPlugin) +{ + const QString subDirName = subDir.dirName(); + // Filter out disabled plugins + if (optVerboseLevel && pluginSelections.disabledPluginTypes.contains(subDirName)) { + std::wcout << "Skipping plugin " << plugin << " due to skipped plugin type " << subDirName << '\n'; + return {}; + } + if (optVerboseLevel && subDirName == u"generic" && plugin.contains(u"qinsighttracker") + && !deployInsightTrackerPlugin) { + std::wcout << "Skipping plugin " << plugin + << ". Use -deploy-insighttracker if you want to use it.\n"; + return {}; + } + if (optVerboseLevel && subDirName == u"tls" && plugin.contains(u"qopensslbackend") + && !deployOpenSslPlugin) { + std::wcout << "Skipping plugin " << plugin + << ". Use -force_openssl or specify -openssl-root if you want to use it.\n"; + return {}; + } + + const int dotIndex = plugin.lastIndexOf(u'.'); + // Strip the .dll from the name, and an additional 'd' if it's a debug library with the 'd' + // suffix + const int stripIndex = debugMatchMode == MatchDebug && platformHasDebugSuffix(platform) + ? dotIndex - 1 + : dotIndex; + const QString pluginName = plugin.first(stripIndex); + + if (optVerboseLevel && pluginSelections.excludedPlugins.contains(pluginName)) { + std::wcout << "Skipping plugin " << plugin << " due to exclusion option" << '\n'; + return {}; + } + + // By default, only deploy qwindows.dll + if (subDirName == u"platforms" + && !(pluginSelections.includedPlugins.contains(pluginName) + || (pluginSelections.enabledPluginTypes.contains(subDirName))) + && !pluginName.startsWith(u"qwindows")) { + return {}; + } + + const QString pluginPath = subDir.absoluteFilePath(plugin); + + // If dueToModule is false, check if the user included the plugin or the entire type. In the + // former's case, only deploy said plugin and not all plugins of that type. + const bool requiresPlugin = pluginSelections.includedPlugins.contains(pluginName) + || pluginSelections.enabledPluginTypes.contains(subDirName); + if (!dueToModule && !requiresPlugin) + return {}; + + // Deploy QUiTools plugins as is without further dependency checking. + // The user needs to ensure all required libraries are present (would + // otherwise pull QtWebEngine for its plugin). + if (subDirName == u"designer") + return pluginPath; + + QStringList dependentQtLibs; + QString errorMessage; + if (findDependentQtLibraries(libraryLocation, pluginPath, platform, + &errorMessage, &dependentQtLibs)) { + for (int d = 0; d < dependentQtLibs.size(); ++d) { + const qint64 module = qtModule(dependentQtLibs.at(d), infix); + if (module >= 0) + (*pluginNeededQtModules)[module] = 1; + } + } else { + std::wcerr << "Warning: Cannot determine dependencies of " + << QDir::toNativeSeparators(pluginPath) << ": " << errorMessage << '\n'; + } + + ModuleBitset disabledNeededQtModules; + disabledNeededQtModules = *pluginNeededQtModules & disabledQtModules; + if (disabledNeededQtModules.any()) { + if (optVerboseLevel) { + std::wcout << "Skipping plugin " << plugin + << " due to disabled dependencies (" + << formatQtModules(disabledNeededQtModules).constData() << ").\n"; + } + return {}; + } + + return pluginPath; +} + +static bool needsPluginType(const QString &subDirName, const PluginInformation &pluginInfo, + const PluginSelections &pluginSelections) +{ + bool needsTypeForPlugin = false; + for (const QString &plugin: pluginSelections.includedPlugins) { + if (pluginInfo.isTypeForPlugin(subDirName, plugin)) + needsTypeForPlugin = true; + } + return (pluginSelections.enabledPluginTypes.contains(subDirName) || needsTypeForPlugin); +} + +QStringList findQtPlugins(ModuleBitset *usedQtModules, const ModuleBitset &disabledQtModules, + const PluginInformation &pluginInfo, const PluginSelections &pluginSelections, + const QString &qtPluginsDirName, const QString &libraryLocation, + const QString &infix, DebugMatchMode debugMatchModeIn, Platform platform, + QString *platformPlugin, bool deployInsightTrackerPlugin, + bool deployOpenSslPlugin) +{ + if (qtPluginsDirName.isEmpty()) + return QStringList(); + QDir pluginsDir(qtPluginsDirName); + QStringList result; + bool missingQtModulesAdded = false; + const QFileInfoList &pluginDirs = pluginsDir.entryInfoList(QStringList(u"*"_s), QDir::Dirs | QDir::NoDotAndDotDot); + for (const QFileInfo &subDirFi : pluginDirs) { + const QString subDirName = subDirFi.fileName(); + const size_t module = qtModuleEntries.moduleIdForPluginType(subDirName); + if (module == QtModule::InvalidId) { + if (optVerboseLevel > 1) { + std::wcerr << "No Qt module found for plugin type \"" << subDirName << "\".\n"; + } + continue; + } + const bool dueToModule = usedQtModules->test(module); + if (dueToModule || needsPluginType(subDirName, pluginInfo, pluginSelections)) { + const DebugMatchMode debugMatchMode = (module == QtWebEngineCoreModuleId) + ? MatchDebugOrRelease // QTBUG-44331: Debug detection does not work for webengine, deploy all. + : debugMatchModeIn; + QDir subDir(subDirFi.absoluteFilePath()); + if (optVerboseLevel) + std::wcout << "Adding in plugin type " << subDirFi.baseName() << " for module: " << qtModuleEntries.moduleById(module).name << '\n'; + + const bool isPlatformPlugin = subDirName == "platforms"_L1; + const QStringList plugins = + findSharedLibraries(subDir, platform, debugMatchMode, QString()); + for (const QString &plugin : plugins) { + ModuleBitset pluginNeededQtModules; + const QString pluginPath = + deployPlugin(plugin, subDir, dueToModule, debugMatchMode, &pluginNeededQtModules, + disabledQtModules, pluginSelections, libraryLocation, infix, + platform, deployInsightTrackerPlugin, deployOpenSslPlugin); + if (!pluginPath.isEmpty()) { + if (isPlatformPlugin && plugin.startsWith(u"qwindows")) + *platformPlugin = subDir.absoluteFilePath(plugin); + result.append(pluginPath); + + const ModuleBitset missingModules = (pluginNeededQtModules & ~*usedQtModules); + if (missingModules.any()) { + *usedQtModules |= missingModules; + missingQtModulesAdded = true; + if (optVerboseLevel) { + std::wcout << "Adding " << formatQtModules(missingModules).constData() + << " for " << plugin << " from plugin type: " << subDirName << '\n'; + } + } + } + } // for filter + } // type matches + } // for plugin folder + + // If missing Qt modules were added during plugin deployment make additional pass, because we may need + // additional plugins. + if (pluginSelections.includeSoftPlugins && missingQtModulesAdded) { + if (optVerboseLevel) { + std::wcout << "Performing additional pass of finding Qt plugins due to updated Qt module list: " + << formatQtModules(*usedQtModules).constData() << "\n"; + } + return findQtPlugins(usedQtModules, disabledQtModules, pluginInfo, pluginSelections, qtPluginsDirName, + libraryLocation, infix, debugMatchModeIn, platform, platformPlugin, + deployInsightTrackerPlugin, deployOpenSslPlugin); + } + + return result; +} + +static QStringList translationNameFilters(const ModuleBitset &modules, const QString &prefix) +{ + QStringList result; + for (const auto &qtModule : qtModuleEntries) { + if (modules.test(qtModule.id) && !qtModule.translationCatalog.isEmpty()) { + const QString name = qtModule.translationCatalog + u'_' + prefix + ".qm"_L1; + if (!result.contains(name)) + result.push_back(name); + } + } + return result; +} + +static bool deployTranslations(const QString &sourcePath, const ModuleBitset &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 : std::as_const(prefixes)) { + arguments.clear(); + const QString targetFile = QStringLiteral("qt_") + prefix + QStringLiteral(".qm"); + arguments.append(QStringLiteral("-o")); + const QString targetFilePath = absoluteTarget + u'/' + targetFile; + if (options.json) + options.json->addFile(sourcePath + u'/' + targetFile, absoluteTarget); + arguments.append(QDir::toNativeSeparators(targetFilePath)); + const QStringList translationFilters = translationNameFilters(usedQtModules, prefix); + if (translationFilters.isEmpty()){ + std::wcerr << "Warning: translation catalogs are all empty, skipping translation deployment\n"; + return true; + } + const QFileInfoList &langQmFiles = sourceDir.entryInfoList(translationFilters); + 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; +} + +static QStringList findFFmpegLibs(const QString &qtBinDir, Platform platform) +{ + const std::vector<QLatin1StringView> ffmpegHints = { "avcodec"_L1, "avformat"_L1, "avutil"_L1, + "swresample"_L1, "swscale"_L1 }; + const QStringList bundledLibs = + findSharedLibraries(qtBinDir, platform, MatchDebugOrRelease, {}); + + QStringList ffmpegLibs; + for (const QLatin1StringView &libHint : ffmpegHints) { + const QStringList ffmpegLib = bundledLibs.filter(libHint, Qt::CaseInsensitive); + + if (ffmpegLib.empty()) { + std::wcerr << "Warning: Cannot find FFmpeg libraries. Multimedia features will not work as expected.\n"; + return {}; + } else if (ffmpegLib.size() != 1u) { + std::wcerr << "Warning: Multiple versions of FFmpeg libraries found. Multimedia features will not work as expected.\n"; + return {}; + } + + const QChar slash(u'/'); + QFileInfo ffmpegLibPath{ qtBinDir + slash + ffmpegLib.front() }; + ffmpegLibs.append(ffmpegLibPath.absoluteFilePath()); + } + + return ffmpegLibs; +} + +// Find the openssl libraries Qt executables depend on. +static QStringList findOpenSslLibraries(const QString &openSslRootDir, Platform platform) +{ + const std::vector<QLatin1StringView> libHints = { "libcrypto"_L1, "libssl"_L1 }; + const QChar slash(u'/'); + const QString openSslBinDir = openSslRootDir + slash + "bin"_L1; + const QStringList openSslRootLibs = + findSharedLibraries(openSslBinDir, platform, MatchDebugOrRelease, {}); + + QStringList result; + for (const QLatin1StringView &libHint : libHints) { + const QStringList lib = openSslRootLibs.filter(libHint, Qt::CaseInsensitive); + + if (lib.empty()) { + std::wcerr << "Warning: Cannot find openssl libraries.\n"; + return {}; + } else if (lib.size() != 1u) { + std::wcerr << "Warning: Multiple versions of openssl libraries found.\n"; + return {}; + } + + QFileInfo libPath{ openSslBinDir + slash + lib.front() }; + result.append(libPath.absoluteFilePath()); + } + + return result; +} + + +struct DeployResult +{ + operator bool() const { return success; } + + bool success = false; + bool isDebug = false; + ModuleBitset directlyUsedQtLibraries; + ModuleBitset usedQtLibraries; + ModuleBitset deployedQtLibraries; +}; + +static QString libraryPath(const QString &libraryLocation, const char *name, + const QString &infix, Platform platform, bool debug) +{ + QString result = libraryLocation + u'/'; + result += QLatin1StringView(name); + result += infix; + if (debug && platformHasDebugSuffix(platform)) + result += u'd'; + result += sharedLibrarySuffix(); + return result; +} + +static QString vcDebugRedistDir() { return QStringLiteral("Debug_NonRedist"); } + +static QString vcRedistDir() +{ + const char vcDirVar[] = "VCINSTALLDIR"; + const QChar slash(u'/'); + 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 findMinGWRuntimePaths(const QString &qtBinDir, Platform platform, const QStringList &runtimeFilters) +{ + //MinGW: Add runtime libraries. Check first for the Qt binary directory, and default to path if nothing is found. + QStringList result; + const bool isClang = platform == WindowsDesktopClangMinGW; + QStringList filters; + const QString suffix = u'*' + sharedLibrarySuffix(); + for (const auto &minGWRuntime : runtimeFilters) + filters.append(minGWRuntime + suffix); + + QFileInfoList dlls = QDir(qtBinDir).entryInfoList(filters, QDir::Files); + if (dlls.isEmpty()) { + std::wcerr << "Warning: Runtime libraries not found in Qt binary folder, defaulting to looking in path\n"; + const QString binaryPath = isClang ? findInPath("clang++.exe"_L1) : findInPath("g++.exe"_L1); + if (binaryPath.isEmpty()) { + std::wcerr << "Warning: Cannot find " << (isClang ? "Clang" : "GCC") << " installation directory, " << (isClang ? "clang++" : "g++") << ".exe must be in the path\n"; + return {}; + } + const QString binaryFolder = QFileInfo(binaryPath).absolutePath(); + dlls = QDir(binaryFolder).entryInfoList(filters, QDir::Files); + } + + for (const QFileInfo &dllFi : dlls) + result.append(dllFi.absoluteFilePath()); + + return result; +} + +static QStringList compilerRunTimeLibs(const QString &qtBinDir, Platform platform, bool isDebug, unsigned short machineArch) +{ + QStringList result; + switch (platform) { + case WindowsDesktopMinGW: { + const QStringList minGWRuntimes = { "*gcc_"_L1, "*stdc++"_L1, "*winpthread"_L1 }; + result.append(findMinGWRuntimePaths(qtBinDir, platform, minGWRuntimes)); + break; + } + case WindowsDesktopClangMinGW: { + const QStringList clangMinGWRuntimes = { "*unwind"_L1, "*c++"_L1 }; + result.append(findMinGWRuntimePaths(qtBinDir, platform, clangMinGWRuntimes)); + break; + } +#ifdef Q_OS_WIN + case WindowsDesktopMsvcIntel: + case WindowsDesktopMsvcArm: { // 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("*.dll"_L1)); + 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 += u'/' + countryCodes.constFirst(); + QFileInfo fi(releaseRedistDir + "/vc_redist."_L1 + + machineArchString + ".exe"_L1); + if (!fi.isFile()) { // Pre MSVC2017/15.5 + fi.setFile(releaseRedistDir + "/vcredist_"_L1 + + machineArchString + ".exe"_L1); + } + 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 = u'.'; + 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; +} + +// 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, + const PluginInformation &pluginInfo, QString *errorMessage) +{ + DeployResult result; + + const QChar slash = u'/'; + + const QString qtBinDir = qtpathsVariables.value(QStringLiteral("QT_INSTALL_BINS")); + const QString libraryLocation = qtBinDir; + const QString infix = qtpathsVariables.value(QLatin1StringView(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). + for (int m = 0; m < dependentQtLibs.size(); ++m) { + const qint64 module = qtModule(dependentQtLibs.at(m), infix); + if (module >= 0) + result.directlyUsedQtLibraries[module] = 1; + } + + const bool usesQml = result.directlyUsedQtLibraries.test(QtQmlModuleId); + const bool usesQuick = result.directlyUsedQtLibraries.test(QtQuickModuleId); + const bool uses3DQuick = result.directlyUsedQtLibraries.test(Qt3DQuickModuleId); + const bool usesQml2 = !(options.disabledLibraries.test(QtQmlModuleId)) + && (usesQml || usesQuick || uses3DQuick || (options.additionalLibraries.test(QtQmlModuleId))); + + 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, 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 + + QLatin1StringView(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 + + QLatin1StringView(windowsSharedLibrarySuffix); + if (!findInPath(icuLibCandidate).isEmpty()) { + icuLib = icuLibCandidate; + } + } + icuLibs.push_back(icuLib); + } + for (const QString &icuLib : std::as_const(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 : std::as_const(qmlDirectories)) { + if (optVerboseLevel >= 1) + std::wcout << "Scanning " << QDir::toNativeSeparators(qmlDirectory) << ":\n"; + const QmlImportScanResult scanResult = + runQmlImportScanner(qmlDirectory, qmlImportPaths, + result.directlyUsedQtLibraries.test(QtWidgetsModuleId), + options.platform, debugMatchMode, errorMessage); + if (!scanResult.ok) + return result; + qmlScanResult.append(scanResult); + // Additional dependencies of QML plugins. + for (const QString &plugin : std::as_const(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 : std::as_const(qmlScanResult.modules)) { + std::wcout << " '" << mod.name << "' " + << QDir::toNativeSeparators(mod.sourcePath) << '\n'; + } + if (optVerboseLevel >= 2) { + std::wcout << "QML plugins:\n"; + for (const QString &p : std::as_const(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) { + const qint64 module = qtModule(dependentQtLibs.at(i), infix); + if (module >= 0) + result.usedQtLibraries[module] = 1; + else + deployedQtLibraries.push_back(dependentQtLibs.at(i)); // Not represented by flag. + } + result.deployedQtLibraries = (result.usedQtLibraries | options.additionalLibraries) & ~options.disabledLibraries; + + ModuleBitset disabled = options.disabledLibraries; + if (!usesQml2) { + disabled[QtQmlModuleId] = 1; + disabled[QtQuickModuleId] = 1; + } + + QStringList openSslLibs; + if (!options.openSslRootDirectory.isEmpty()) { + openSslLibs = findOpenSslLibraries(options.openSslRootDirectory, options.platform); + if (openSslLibs.isEmpty()) { + *errorMessage = QStringLiteral("Unable to find openSSL libraries in ") + + options.openSslRootDirectory; + return result; + } + + deployedQtLibraries.append(openSslLibs); + } + const bool deployOpenSslPlugin = options.forceOpenSslPlugin || !openSslLibs.isEmpty(); + + const QStringList plugins = findQtPlugins( + &result.deployedQtLibraries, + // For non-QML applications, disable QML to prevent it from being pulled in by the + // qtaccessiblequick plugin. + disabled, pluginInfo, + options.pluginSelections, qtpathsVariables.value(QStringLiteral("QT_INSTALL_PLUGINS")), + libraryLocation, infix, debugMatchMode, options.platform, &platformPlugin, + options.deployInsightTrackerPlugin, deployOpenSslPlugin); + + // Apply options flags and re-add library names. + QString qtGuiLibrary; + for (const auto &qtModule : qtModuleEntries) { + if (result.deployedQtLibraries.test(qtModule.id)) { + const QString library = libraryPath(libraryLocation, qtModule.name.toUtf8(), infix, + options.platform, result.isDebug); + deployedQtLibraries.append(library); + if (qtModule.id == QtGuiModuleId) + 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(u',') << '\n'; + + if (result.deployedQtLibraries.test(QtGuiModuleId) && platformPlugin.isEmpty()) { + *errorMessage =QStringLiteral("Unable to find the platform plugin."); + return result; + } + + if (options.platform.testFlag(WindowsBased) && !qtGuiLibrary.isEmpty()) { + const QStringList guiLibraries = findDependentLibraries(qtGuiLibrary, errorMessage); + const bool dependsOnOpenGl = !guiLibraries.filter(QStringLiteral("opengl32"), Qt::CaseInsensitive).isEmpty(); + if (options.softwareRasterizer && !dependsOnOpenGl) { + const QFileInfo softwareRasterizer(qtBinDir + slash + QStringLiteral("opengl32sw") + QLatin1StringView(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); + } + } + if (options.systemDxc) { + const QStringList dxcLibs = findDxc(options.platform, qtBinDir, wordSize); + if (!dxcLibs.isEmpty()) + deployedQtLibraries.append(dxcLibs); + else + std::wcerr << "Warning: Cannot find any version of the dxcompiler.dll and dxil.dll.\n"; + } + } // Windows + + // Add FFmpeg if we deploy the FFmpeg backend + if (options.ffmpeg + && !plugins.filter(QStringLiteral("ffmpegmediaplugin"), Qt::CaseInsensitive).empty()) { + deployedQtLibraries.append(findFFmpegLibs(qtBinDir, options.platform)); + } + + // Update libraries + if (options.libraries) { + const QString targetPath = options.libraryDirectory.isEmpty() ? + options.directory : options.libraryDirectory; + QStringList libraries = deployedQtLibraries; + if (options.compilerRunTime) + libraries.append(compilerRunTimeLibs(qtBinDir, options.platform, result.isDebug, machineArch)); + for (const QString &qtLib : std::as_const(libraries)) { + if (!updateLibrary(qtLib, targetPath, options, errorMessage)) + return result; + } + +#if !QT_CONFIG(relocatable) + if (options.patchQt && !options.dryRun) { + const QString qt6CoreName = QFileInfo(libraryPath(libraryLocation, "Qt6Core", infix, + options.platform, result.isDebug)).fileName(); + if (!patchQtCore(targetPath + u'/' + qt6CoreName, errorMessage)) { + std::wcerr << "Warning: " << *errorMessage << '\n'; + errorMessage->clear(); + } + } +#endif // QT_CONFIG(relocatable) + } // 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 = "Cannot create "_L1 + + QDir::toNativeSeparators(dir.absolutePath()) + u'.'; + 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 + u'.'; + return result; + } + } + if (!updateLibrary(plugin, targetPath, options, errorMessage)) + return result; + } + } // optPlugins + + // Update Quick imports + // 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 && usesQml2) { + const QString targetPath = options.qmlDirectory.isEmpty() + ? options.directory + QStringLiteral("/qml") + : options.qmlDirectory; + if (!createDirectory(targetPath, errorMessage, options.dryRun)) + return result; + for (const QmlImportScanResult::Module &module : std::as_const(qmlScanResult.modules)) { + const QString installPath = module.installPath(targetPath); + if (optVerboseLevel > 1) + std::wcout << "Installing: '" << module.name + << "' from " << module.sourcePath << " to " + << QDir::toNativeSeparators(installPath) << '\n'; + if (installPath != targetPath && !createDirectory(installPath, errorMessage, options.dryRun)) + return result; + unsigned updateFileFlags = options.updateFileFlags + | SkipQmlDesignerSpecificsDirectories; + unsigned qmlDirectoryFileFlags = 0; + if (options.deployPdb) + qmlDirectoryFileFlags |= QmlDirectoryFileEntryFunction::DeployPdb; + if (!updateFile(module.sourcePath, QmlDirectoryFileEntryFunction(module.sourcePath, + options.platform, + debugMatchMode, + qmlDirectoryFileFlags), + installPath, updateFileFlags, options.json, errorMessage)) { + return result; + } + } + } // optQuickImports + + if (options.translations) { + if (!createDirectory(options.translationsDirectory, errorMessage, options.dryRun)) + 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 PluginInformation &pluginInfo, 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")) + + u'/' + webProcess; + if (!updateFile(webProcessSource, sourceOptions.directory, sourceOptions.updateFileFlags, sourceOptions.json, errorMessage)) + return false; + Options options(sourceOptions); + options.binaries.append(options.directory + u'/' + webProcess); + options.quickImports = false; + options.translations = false; + return deploy(options, qtpathsVariables, pluginInfo, errorMessage); +} + +static bool deployWebEngineCore(const QMap<QString, QString> &qtpathsVariables, + const PluginInformation &pluginInfo, 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", + isDebug ? "v8_context_snapshot.debug.bin" + : "v8_context_snapshot.bin" }; + QByteArray webEngineProcessName(webEngineProcessC); + if (isDebug && platformHasDebugSuffix(options.platform)) + webEngineProcessName.append('d'); + if (optVerboseLevel) + std::wcout << "Deploying: " << webEngineProcessName.constData() << "...\n"; + if (!deployWebProcess(qtpathsVariables, webEngineProcessName, pluginInfo, options, errorMessage)) + return false; + const QString resourcesSubDir = QStringLiteral("/resources"); + const QString resourcesSourceDir = qtpathsVariables.value(QStringLiteral("QT_INSTALL_DATA")) + + resourcesSubDir + u'/'; + const QString resourcesTargetDir(options.directory + resourcesSubDir); + if (!createDirectory(resourcesTargetDir, errorMessage, options.dryRun)) + return false; + for (auto installDataFile : installDataFiles) { + if (!updateFile(resourcesSourceDir + QLatin1StringView(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, options.dryRun) + && 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 + u'/' + + translations.fileName(); + if (!createDirectory(webEngineTranslationsDir, errorMessage, options.dryRun)) + 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(QT_VERSION_STR ""_L1); + + 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.prepend(QDir::listSeparator().toLatin1()); + path.prepend(qtBinPath); + qputenv("PATH", path); + } + + Options options; + QString errorMessage; + + // Early parse the --qmake and --qtpaths options, because they are needed to determine the + // options that select/deselect Qt modules. + { + int result = parseEarlyArguments(QCoreApplication::arguments(), &options, &errorMessage); + if (result & CommandLineParseError) { + std::wcerr << "Error: " << errorMessage << "\n"; + return 1; + } + } + + const QMap<QString, QString> qtpathsVariables = + queryQtPaths(options.qtpathsBinary, &errorMessage); + const QString xSpec = qtpathsVariables.value(QStringLiteral("QMAKE_XSPEC")); + if (qtpathsVariables.isEmpty() || xSpec.isEmpty() + || !qtpathsVariables.contains(QStringLiteral("QT_INSTALL_BINS"))) { + std::wcerr << "Unable to query qtpaths: " << errorMessage << '\n'; + return 1; + } + + options.platform = platformFromMkSpec(xSpec); + if (options.platform == UnknownPlatform) { + std::wcerr << "Unsupported platform " << xSpec << '\n'; + return 1; + } + + // Read the Qt module information from the Qt installation directory. + const QString modulesDir + = qtpathsVariables.value(QLatin1String("QT_INSTALL_ARCHDATA")) + + QLatin1String("/modules"); + const QString translationsDir + = qtpathsVariables.value(QLatin1String("QT_INSTALL_TRANSLATIONS")); + if (!qtModuleEntries.populate(modulesDir, translationsDir, optVerboseLevel > 1, + &errorMessage)) { + std::wcerr << "Error: " << errorMessage << "\n"; + return 1; + } + assignKnownModuleIds(); + + // Read the Qt plugin types information from the Qt installation directory. + PluginInformation pluginInfo{}; + pluginInfo.generateAvailablePlugins(qtpathsVariables, options.platform); + + // Parse the full 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 & CommandLineVersionRequested) { + std::fputs(QT_VERSION_STR "\n", stdout); + return 0; + } + if (result & CommandLineParseHelpRequested) + std::fputs(qPrintable(helpText(parser, pluginInfo)), stdout); + if (result & CommandLineParseError) + return 1; + if (result & CommandLineParseHelpRequested) + return 0; + } + + // Create directories + if (!createDirectory(options.directory, &errorMessage, options.dryRun)) { + std::wcerr << errorMessage << '\n'; + return 1; + } + if (!options.libraryDirectory.isEmpty() && options.libraryDirectory != options.directory + && !createDirectory(options.libraryDirectory, &errorMessage, options.dryRun)) { + std::wcerr << errorMessage << '\n'; + return 1; + } + + const DeployResult result = deploy(options, qtpathsVariables, pluginInfo, &errorMessage); + if (!result) { + std::wcerr << errorMessage << '\n'; + return 1; + } + + if (result.deployedQtLibraries.test(QtWebEngineCoreModuleId)) { + if (!deployWebEngineCore(qtpathsVariables, pluginInfo, 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..a7e63e7470 --- /dev/null +++ b/src/tools/windeployqt/qmlutils.cpp @@ -0,0 +1,138 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#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 + +using namespace Qt::StringLiterals; + +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 qsizetype lastSlashPos = relativePath.lastIndexOf(u'/'); + if (lastSlashPos != -1) { + result += u'/'; + 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()); + // Don't enter other QML modules when recursing! + if (subDirectory.isReadable() && !subDirectory.exists(QStringLiteral("qmldir"))) + 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) + u'"'; + 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() == "module"_L1) { + 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..bff1fb3a9b --- /dev/null +++ b/src/tools/windeployqt/qmlutils.h @@ -0,0 +1,40 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#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/qtmoduleinfo.cpp b/src/tools/windeployqt/qtmoduleinfo.cpp new file mode 100644 index 0000000000..b928a64478 --- /dev/null +++ b/src/tools/windeployqt/qtmoduleinfo.cpp @@ -0,0 +1,183 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "qtmoduleinfo.h" +#include "utils.h" + +#include <QDirListing> +#include <QJsonDocument> +#include <QJsonArray> +#include <QDebug> + +#include <iostream> +#include <algorithm> +#include <unordered_map> + +using namespace Qt::StringLiterals; + +static QStringList toStringList(const QJsonArray &jsonArray) +{ + QStringList result; + for (const auto &item : jsonArray) { + if (item.isString()) + result.append(item.toString()); + } + return result; +} + +struct TranslationCatalog +{ + QString name; + QStringList repositories; + QStringList modules; +}; + +using TranslationCatalogs = std::vector<TranslationCatalog>; + +static TranslationCatalogs readTranslationsCatalogs(const QString &translationsDir, + bool verbose, + QString *errorString) +{ + QFile file(translationsDir + QLatin1String("/catalogs.json")); + if (verbose) { + std::wcerr << "Trying to read translation catalogs from \"" + << qUtf8Printable(file.fileName()) << "\".\n"; + } + if (!file.open(QIODevice::ReadOnly)) { + *errorString = QLatin1String("Cannot open ") + file.fileName(); + return {}; + } + + QJsonParseError jsonParseError; + QJsonDocument document = QJsonDocument::fromJson(file.readAll(), &jsonParseError); + if (jsonParseError.error != QJsonParseError::NoError) { + *errorString = jsonParseError.errorString(); + return {}; + } + + if (!document.isArray()) { + *errorString = QLatin1String("Expected an array as root element of ") + file.fileName(); + return {}; + } + + TranslationCatalogs catalogs; + for (const QJsonValueRef &item : document.array()) { + TranslationCatalog catalog; + catalog.name = item[QLatin1String("name")].toString(); + catalog.repositories = toStringList(item[QLatin1String("repositories")].toArray()); + catalog.modules = toStringList(item[QLatin1String("modules")].toArray()); + if (verbose) + std::wcerr << "Found catalog \"" << qUtf8Printable(catalog.name) << "\".\n"; + catalogs.emplace_back(std::move(catalog)); + } + + return catalogs; +} + +static QtModule moduleFromJsonFile(const QString &filePath, QString *errorString) +{ + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) { + *errorString = QLatin1String("Cannot open ") + file.fileName(); + return {}; + } + + QJsonParseError jsonParseError; + QJsonDocument document = QJsonDocument::fromJson(file.readAll(), &jsonParseError); + if (jsonParseError.error != QJsonParseError::NoError) { + *errorString = jsonParseError.errorString(); + return {}; + } + + if (!document.isObject()) { + *errorString = QLatin1String("Expected an object as root element of ") + file.fileName(); + return {}; + } + + const QJsonObject obj = document.object(); + QtModule module; + module.name = "Qt6"_L1 + obj[QLatin1String("name")].toString(); + module.repository = obj[QLatin1String("repository")].toString(); + module.internal = obj[QLatin1String("internal")].toBool(); + module.pluginTypes = toStringList(obj[QLatin1String("plugin_types")].toArray()); + return module; +} + +static void dump(const QtModule &module) +{ + std::wcerr << "Found module \"" << qUtf8Printable(module.name) << "\".\n"; + if (!module.pluginTypes.isEmpty()) + qDebug().nospace() << " plugin types: " << module.pluginTypes; + if (!module.translationCatalog.isEmpty()) + qDebug().nospace() << " translation catalog: "<< module.translationCatalog; +} + +bool QtModuleInfoStore::populate(const QString &modulesDir, const QString &translationsDir, + bool verbose, QString *errorString) +{ + const TranslationCatalogs catalogs = readTranslationsCatalogs(translationsDir, verbose, + errorString); + if (!errorString->isEmpty()) { + std::wcerr << "Warning: Translations will not be available due to the following error." + << std::endl << *errorString << std::endl; + errorString->clear(); + } + std::unordered_map<QString, QString> moduleToCatalogMap; + std::unordered_map<QString, QString> repositoryToCatalogMap; + for (const TranslationCatalog &catalog : catalogs) { + for (const QString &module : catalog.modules) { + moduleToCatalogMap.insert(std::make_pair(module, catalog.name)); + } + for (const QString &repository : catalog.repositories) { + repositoryToCatalogMap.insert(std::make_pair(repository, catalog.name)); + } + } + + // Read modules, and assign a bit as ID. + for (const auto &dirEntry : QDirListing(modulesDir, {u"*.json"_s}, QDir::Files)) { + QtModule module = moduleFromJsonFile(dirEntry.filePath(), errorString); + if (!errorString->isEmpty()) + return false; + if (module.internal && module.name.endsWith(QStringLiteral("Private"))) + module.name.chop(7); + module.id = modules.size(); + if (module.id == QtModule::InvalidId) { + *errorString = "Internal Error: too many modules for ModuleBitset to hold."_L1; + return false; + } + + { + auto it = moduleToCatalogMap.find(module.name); + if (it != moduleToCatalogMap.end()) + module.translationCatalog = it->second; + } + if (module.translationCatalog.isEmpty()) { + auto it = repositoryToCatalogMap.find(module.repository); + if (it != repositoryToCatalogMap.end()) + module.translationCatalog = it->second; + } + if (verbose) + dump(module); + modules.emplace_back(std::move(module)); + } + + return true; +} + +const QtModule &QtModuleInfoStore::moduleById(size_t id) const +{ + return modules.at(id); +} + +size_t QtModuleInfoStore::moduleIdForPluginType(const QString &pluginType) const +{ + auto moduleHasPluginType = [&pluginType] (const QtModule &module) { + return module.pluginTypes.contains(pluginType); + }; + + auto it = std::find_if(modules.begin(), modules.end(), moduleHasPluginType); + if (it != modules.end()) + return it->id ; + + return QtModule::InvalidId; +} diff --git a/src/tools/windeployqt/qtmoduleinfo.h b/src/tools/windeployqt/qtmoduleinfo.h new file mode 100644 index 0000000000..b35403a090 --- /dev/null +++ b/src/tools/windeployqt/qtmoduleinfo.h @@ -0,0 +1,51 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef QTMODULEINFO_H +#define QTMODULEINFO_H + +#include <QString> +#include <QStringList> + +#include <bitset> +#include <vector> + +constexpr size_t ModuleBitsetSize = 1024; +using ModuleBitset = std::bitset<ModuleBitsetSize>; + +struct QtModule +{ + static constexpr size_t InvalidId = ModuleBitsetSize - 1; + size_t id = InvalidId; + bool internal = false; + QString name; + QString repository; + QStringList pluginTypes; + QString translationCatalog; +}; + +inline bool contains(const ModuleBitset &modules, const QtModule &module) +{ + return modules.test(module.id); +} + +class QtModuleInfoStore +{ +public: + QtModuleInfoStore() = default; + + bool populate(const QString &modulesDir, const QString &translationsDir, bool verbose, + QString *errorString); + + size_t size() const { return modules.size(); } + std::vector<QtModule>::const_iterator begin() const { return modules.begin(); } + std::vector<QtModule>::const_iterator end() const { return modules.end(); } + + const QtModule &moduleById(size_t id) const; + size_t moduleIdForPluginType(const QString &pluginType) const; + +private: + std::vector<QtModule> modules; +}; + +#endif diff --git a/src/tools/windeployqt/qtplugininfo.cpp b/src/tools/windeployqt/qtplugininfo.cpp new file mode 100644 index 0000000000..1deaa35f35 --- /dev/null +++ b/src/tools/windeployqt/qtplugininfo.cpp @@ -0,0 +1,100 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "qtplugininfo.h" + +#include <QDir> + +static PluginDetection determinePluginLibrary(const QDir &platformPluginDir, const QString &infix) +{ + // Use the platform plugin to determine which dlls are there (release/debug/both) + QString platformReleaseFilter(QStringLiteral("qwindows")); + if (!infix.isEmpty()) + platformReleaseFilter += infix; + QString platformFilter = platformReleaseFilter + u'*'; + platformFilter += sharedLibrarySuffix(); + + const QFileInfoList &dlls = + platformPluginDir.entryInfoList(QStringList(platformFilter), QDir::Files); + if (dlls.size() == 1) { + const QFileInfo dllFi = dlls.first(); + const bool hasDebugDlls = + dllFi.fileName() == QString(platformReleaseFilter + sharedLibrarySuffix()) ? false + : true; + return (hasDebugDlls ? PluginDetection::DebugOnly : PluginDetection::ReleaseOnly); + } else { + return PluginDetection::DebugAndRelease; + } +} + +static QStringList findPluginNames(const QDir &pluginDir, const PluginDetection libraryType, + const Platform &platform) +{ + QString errorMessage{}; + QStringList result{}; + QString filter{}; + filter += u"*"; + filter += sharedLibrarySuffix(); + + const QFileInfoList &dlls = + pluginDir.entryInfoList(QStringList(filter), QDir::Files, QDir::Name); + + for (const QFileInfo &dllFi : dlls) { + QString plugin = dllFi.fileName(); + const int dotIndex = plugin.lastIndexOf(u'.'); + // We don't need the .dll for the name + plugin = plugin.first(dotIndex); + + if (libraryType == PluginDetection::DebugAndRelease) { + bool isDebugDll{}; + if (!readPeExecutable(dllFi.absoluteFilePath(), &errorMessage, 0, 0, &isDebugDll, + (platform == WindowsDesktopMinGW))) { + std::wcerr << "Warning: Unable to read " + << QDir::toNativeSeparators(dllFi.absoluteFilePath()) << ": " + << errorMessage; + } + if (isDebugDll && platformHasDebugSuffix(platform)) + plugin.removeLast(); + } + else if (libraryType == PluginDetection::DebugOnly) + plugin.removeLast(); + + if (!result.contains(plugin)) + result.append(plugin); + } + return result; +} + +bool PluginInformation::isTypeForPlugin(const QString &type, const QString &plugin) const +{ + return m_pluginMap.at(plugin) == type; +} + +void PluginInformation::populatePluginToType(const QDir &pluginDir, const QStringList &plugins) +{ + for (const QString &plugin : plugins) + m_pluginMap.insert({ plugin, pluginDir.dirName() }); +} + +void PluginInformation::generateAvailablePlugins(const QMap<QString, QString> &qtPathsVariables, + const Platform &platform) +{ + const QDir pluginTypesDir(qtPathsVariables.value(QLatin1String("QT_INSTALL_PLUGINS"))); + const QDir platformPluginDir(pluginTypesDir.absolutePath() + QStringLiteral("/platforms")); + const QString infix(qtPathsVariables.value(QLatin1String(qmakeInfixKey))); + const PluginDetection debugDetection = determinePluginLibrary(platformPluginDir, infix); + + const QFileInfoList &pluginTypes = + pluginTypesDir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); + for (const QFileInfo &pluginType : pluginTypes) { + const QString pluginTypeName = pluginType.baseName(); + m_typeMap.insert({ pluginTypeName, QStringList{} }); + const QStringList plugins = + findPluginNames(pluginType.absoluteFilePath(), debugDetection, platform); + m_typeMap.at(pluginTypeName) = plugins; + populatePluginToType(pluginTypeName, plugins); + } + if (!m_typeMap.size() || !m_pluginMap.size()) + std::wcerr << "Warning: could not parse available plugins properly, plugin " + "inclusion/exclusion options will not work\n"; +} diff --git a/src/tools/windeployqt/qtplugininfo.h b/src/tools/windeployqt/qtplugininfo.h new file mode 100644 index 0000000000..420b2b5e1a --- /dev/null +++ b/src/tools/windeployqt/qtplugininfo.h @@ -0,0 +1,48 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#ifndef QTPLUGININFO_H +#define QTPLUGININFO_H + +#include "utils.h" + +#include <QString> +#include <QStringList> + +#include <unordered_map> + +enum class PluginDetection +{ + DebugOnly, + ReleaseOnly, + DebugAndRelease +}; + +struct PluginSelections +{ + QStringList disabledPluginTypes; + QStringList enabledPluginTypes; + QStringList excludedPlugins; + QStringList includedPlugins; + bool includeSoftPlugins = false; +}; + +class PluginInformation +{ +public: + PluginInformation() = default; + + bool isTypeForPlugin(const QString &type, const QString &plugin) const; + + void generateAvailablePlugins(const QMap<QString, QString> &qtPathsVariables, + const Platform &platform); + void populatePluginToType(const QDir &pluginDir, const QStringList &plugins); + + const std::unordered_map<QString, QStringList> &typeMap() const { return m_typeMap; } + +private: + std::unordered_map<QString, QStringList> m_typeMap; + std::unordered_map<QString, QString> m_pluginMap; +}; + +#endif diff --git a/src/tools/windeployqt/utils.cpp b/src/tools/windeployqt/utils.cpp new file mode 100644 index 0000000000..5141119254 --- /dev/null +++ b/src/tools/windeployqt/utils.cpp @@ -0,0 +1,1022 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include "utils.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 <QtCore/private/qsystemerror_p.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 + +using namespace Qt::StringLiterals; + +int optVerboseLevel = 1; + +bool isBuildDirectory(Platform platform, const QString &dirName) +{ + return (platform.testFlag(Msvc) || platform.testFlag(ClangMsvc)) + && (dirName == "debug"_L1 || dirName == "release"_L1); +} + +// 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, bool dryRun) +{ + 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"; + if (!dryRun) { + 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 += u'*'; + if (debugMatchMode == MatchDebug && platformHasDebugSuffix(platform)) + nameFilter += u'd'; + nameFilter += sharedLibrarySuffix(); + 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 + +// 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(u';'); + const QByteArray sdkDir = qgetenv("WindowsSdkDir"); + if (!sdkDir.isEmpty()) + paths.prepend(QDir::cleanPath(QString::fromLocal8Bit(sdkDir)) + "/Tools/x64"_L1); + 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(u' '); + if (!commandLine->isEmpty()) + commandLine->append(u' '); + if (needsQuote) + commandLine->append(u'"'); + commandLine->append(argument); + if (needsQuote) + commandLine->append(u'"'); +} + +// 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 = u'\\'; + 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: ") + + QSystemError::windowsString(); + } + 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(u'/')) + path += u'/'; + 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 : std::as_const(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"); + const QString colonSpace = QStringLiteral(": "); + QByteArray stdOut; + QByteArray stdErr; + unsigned long exitCode = 0; + if (!runProcess(binary, QStringList(QStringLiteral("-query")), QString(), &exitCode, &stdOut, + &stdErr, errorMessage)) { + *errorMessage = QStringLiteral("Error running binary ") + binary + colonSpace + *errorMessage; + return QMap<QString, QString>(); + } + if (exitCode) { + *errorMessage = binary + QStringLiteral(" returns ") + QString::number(exitCode) + + colonSpace + QString::fromLocal8Bit(stdErr); + return QMap<QString, QString>(); + } + const QString output = QString::fromLocal8Bit(stdOut).trimmed().remove(u'\r'); + QMap<QString, QString> result; + const qsizetype size = output.size(); + for (qsizetype pos = 0; pos < size; ) { + const qsizetype colonPos = output.indexOf(u':', pos); + if (colonPos < 0) + break; + qsizetype endPos = output.indexOf(u'\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(QLatin1StringView(qmakeInfixKey), infix); + } + break; + } + } + } else { + std::wcerr << "Warning: Unable to read " << QDir::toNativeSeparators(qconfigPriFile.fileName()) + << colonSpace << 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 + u'/' + 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; +} + +#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) { + qsizetype pos = 0; + if (lib.startsWith("MSVCR"_L1, Qt::CaseInsensitive) + || lib.startsWith("MSVCP"_L1, Qt::CaseInsensitive) + || lib.startsWith("VCRUNTIME"_L1, Qt::CaseInsensitive) + || lib.startsWith("VCCORLIB"_L1, Qt::CaseInsensitive) + || lib.startsWith("CONCRT"_L1, Qt::CaseInsensitive) + || lib.startsWith("UCRTBASE"_L1, Qt::CaseInsensitive)) { + qsizetype lastDotPos = lib.lastIndexOf(u'.'); + pos = -1 == lastDotPos ? 0 : lastDotPos - 1; + } + + if (pos > 0) { + const auto removeExtraSuffix = [&lib, &pos](const QString &suffix) -> void { + if (lib.contains(suffix, Qt::CaseInsensitive)) + pos -= suffix.size(); + }; + removeExtraSuffix("_app"_L1); + removeExtraSuffix("_atomic_wait"_L1); + removeExtraSuffix("_codecvt_ids"_L1); + } + + if (pos) + return lib.at(pos).toLower() == u'd' ? MsvcDebugRuntime : MsvcReleaseRuntime; + } + return NoMsvcRuntime; +} + +template <class ImageNtHeader> +inline QStringList determineDependentLibs(const ImageNtHeader *nth, const void *fileMemory, + QString *errorMessage) +{ + return readImportSections(nth, fileMemory, errorMessage); +} + +template <class ImageNtHeader> +inline bool determineDebug(const ImageNtHeader *nth, const void *fileMemory, + QStringList *dependentLibrariesIn, QString *errorMessage) +{ + if (nth->FileHeader.Characteristics & IMAGE_FILE_DEBUG_STRIPPED) + return false; + + const QStringList dependentLibraries = dependentLibrariesIn != nullptr ? + *dependentLibrariesIn : + determineDependentLibs(nth, fileMemory, errorMessage); + + const bool hasDebugEntry = nth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG].Size; + // When an MSVC debug entry is present, check whether the debug runtime + // is actually used to detect -release / -force-debug-info builds. + const MsvcDebugRuntimeResult msvcrt = checkMsvcDebugRuntime(dependentLibraries); + if (msvcrt == NoMsvcRuntime) + return hasDebugEntry; + else + return hasDebugEntry && msvcrt == MsvcDebugRuntime; +} + +template <class ImageNtHeader> +inline void determineDebugAndDependentLibs(const ImageNtHeader *nth, const void *fileMemory, + QStringList *dependentLibrariesIn, + bool *isDebugIn, QString *errorMessage) +{ + if (dependentLibrariesIn) + *dependentLibrariesIn = determineDependentLibs(nth, fileMemory, errorMessage); + + if (isDebugIn) + *isDebugIn = determineDebug(nth, fileMemory, dependentLibrariesIn, errorMessage); +} + +// 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, QSystemError::windowsString()); + 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, QSystemError::windowsString()); + break; + } + + fileMemory = MapViewOfFile(hFileMap, FILE_MAP_READ, 0, 0, 0); + if (!fileMemory) { + *errorMessage = QString::fromLatin1("Cannot map '%1': %2") + .arg(peExecutableFileName, QSystemError::windowsString()); + 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, dependentLibrariesIn, isDebugIn, errorMessage); + } else { + determineDebugAndDependentLibs(reinterpret_cast<const IMAGE_NT_HEADERS64 *>(ntHeaders), + fileMemory, 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(u' '); + 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 = QLatin1StringView(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 + u'*' + 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 : std::as_const(candidateVersions)) { + const QFileInfo fi(qtBinDir + u'/' + 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 : std::as_const(candidateVersions)) { + const QString dll = findInPath(candidate); + if (!dll.isEmpty() + && readPeExecutable(dll, &errorMessage, 0, &detectedWordSize, 0) + && detectedWordSize == wordSize) { + return dll; + } + } + } + return QString(); +} + +QStringList findDxc(Platform platform, const QString &qtBinDir, unsigned wordSize) +{ + QStringList results; + const QString kitDir = QString::fromLocal8Bit(qgetenv("WindowsSdkDir")); + const QString suffix = QLatin1StringView(windowsSharedLibrarySuffix); + for (QString prefix : { QStringLiteral("dxcompiler"), QStringLiteral("dxil") }) { + QString name = prefix + suffix; + if (!kitDir.isEmpty()) { + QString redistDirPath = QDir::cleanPath(kitDir) + QStringLiteral("/Redist/D3D/"); + if (platform.testFlag(ArmBased)) { + redistDirPath += wordSize == 32 ? QStringLiteral("arm") : QStringLiteral("arm64"); + } else { + redistDirPath += wordSize == 32 ? QStringLiteral("x86") : QStringLiteral("x64"); + } + QDir redistDir(redistDirPath); + if (redistDir.exists()) { + const QFileInfoList files = redistDir.entryInfoList(QStringList(prefix + u'*' + suffix), QDir::Files); + if (!files.isEmpty()) { + results.append(files.front().absoluteFilePath()); + continue; + } + } + } + // Check the bin directory of the Qt SDK (in case it is shadowed by the + // Windows system directory in PATH). + const QFileInfo fi(qtBinDir + u'/' + name); + if (fi.isFile()) { + results.append(fi.absoluteFilePath()); + continue; + } + // Try to find it in the PATH (e.g. the Vulkan SDK ships these, even if Windows itself doesn't). + if (platform.testFlag(IntelBased)) { + QString errorMessage; + unsigned detectedWordSize; + const QString dll = findInPath(name); + if (!dll.isEmpty() + && readPeExecutable(dll, &errorMessage, 0, &detectedWordSize, 0) + && detectedWordSize == wordSize) + { + results.append(dll); + continue; + } + } + } + return results; +} + +#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(); +} + +QStringList findDxc(Platform, const QString &, unsigned) +{ + return QStringList(); +} + +#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..fb3ba0b40b --- /dev/null +++ b/src/tools/windeployqt/utils.h @@ -0,0 +1,366 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#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, + // CPU + IntelBased = 0x00010, + ArmBased = 0x00020, + // Compiler + Msvc = 0x00100, + MinGW = 0x00200, + ClangMsvc = 0x00400, + ClangMinGW = 0x00800, + // Platforms + WindowsDesktopMsvc = WindowsBased + Msvc, + WindowsDesktopMsvcIntel = WindowsDesktopMsvc + IntelBased, + WindowsDesktopMsvcArm = WindowsDesktopMsvc + ArmBased, + WindowsDesktopMinGW = WindowsBased + IntelBased + MinGW, + WindowsDesktopClangMsvc = WindowsBased + IntelBased + ClangMsvc, + WindowsDesktopClangMinGW = WindowsBased + IntelBased + ClangMinGW, + 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 = std::pair<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"; + +inline QString sharedLibrarySuffix() { return QLatin1StringView(windowsSharedLibrarySuffix); } +bool isBuildDirectory(Platform platform, const QString &dirName); + +bool createSymbolicLink(const QFileInfo &source, const QString &target, QString *errorMessage); +bool createDirectory(const QString &directory, QString *errorMessage, bool dryRun); +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); + +#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, QString *errorMessage) +{ + QStringList result; + readPeExecutable(executableFileName, errorMessage, &result); + return result; +} + +QString findD3dCompiler(Platform platform, const QString &qtBinDir, unsigned wordSize); +QStringList findDxc(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 + u'/' + 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(u'/')) { + *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 + u'/' + relativeSource), sourceFileInfo.fileName(), errorMessage); + } // Source is symbolic link + + if (sourceFileInfo.isDir()) { + if ((flags & SkipQmlDesignerSpecificsDirectories) && sourceFileInfo.fileName() == QLatin1StringView("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 + u'/' + 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() == QLatin1StringView("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 |