diff options
Diffstat (limited to 'src/tools/androiddeployqt')
-rw-r--r-- | src/tools/androiddeployqt/CMakeLists.txt | 13 | ||||
-rw-r--r-- | src/tools/androiddeployqt/doc/src/androiddeployqt.qdoc | 231 | ||||
-rw-r--r-- | src/tools/androiddeployqt/main.cpp | 745 |
3 files changed, 516 insertions, 473 deletions
diff --git a/src/tools/androiddeployqt/CMakeLists.txt b/src/tools/androiddeployqt/CMakeLists.txt index da4c524456..041d883877 100644 --- a/src/tools/androiddeployqt/CMakeLists.txt +++ b/src/tools/androiddeployqt/CMakeLists.txt @@ -1,4 +1,5 @@ -# Generated from androiddeployqt.pro. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## androiddeployqt App: @@ -8,14 +9,17 @@ qt_get_tool_target_name(target_name androiddeployqt) qt_internal_add_tool(${target_name} TARGET_DESCRIPTION "Qt Android Deployment Tool" TOOLS_TARGET Core + USER_FACING + INSTALL_VERSIONED_LINK SOURCES main.cpp DEFINES QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII QT_NO_FOREACH - PUBLIC_LIBRARIES - Qt::Core # special case + QT_NO_QPAIR + LIBRARIES + Qt::Core INCLUDE_DIRECTORIES ../shared ) @@ -24,9 +28,6 @@ set_target_properties(${target_name} PROPERTIES WIN32_EXECUTABLE FALSE ) -#### Keys ignored in scope 1:.:.:androiddeployqt.pro:<TRUE>: -# _OPTION = "host_build" - ## Scopes: ##################################################################### diff --git a/src/tools/androiddeployqt/doc/src/androiddeployqt.qdoc b/src/tools/androiddeployqt/doc/src/androiddeployqt.qdoc index 8230d3a398..b94d7f5c04 100644 --- a/src/tools/androiddeployqt/doc/src/androiddeployqt.qdoc +++ b/src/tools/androiddeployqt/doc/src/androiddeployqt.qdoc @@ -1,4 +1,4 @@ -// Copyright (C) 2021 The Qt Company Ltd. +// Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only /*! @@ -7,158 +7,47 @@ \title The androiddeployqt Tool \target androiddeployqt - Building an application package is complex, so Qt comes with a tool which - handles the work for you. The steps described in - \l{Deploying an Application on Android} are handled by the androiddeployqt - tool. + Building an Android package involves many steps, so Qt comes with a tool which + handles the work for you. The steps handled by the androiddeployqt + tool are described in \l{Deploying an Application on Android}. \section1 Prerequisites Before Running androiddeployqt - Before running the tool manually, you need to run \c qmake or \c CMake - on your project to generate \c Makefiles and a \c JSON file (i.e. - \c{android-project-deployment-settings.json}) containing important settings + Before running the tool manually, you need to configure your project with + \c CMake or \c qmake to generate \c Makefiles and a \c JSON file (i.e. + \c{android-<target_name>-deployment-settings.json}) containing important settings used by \c androiddeployqt. \note It is not recommended to modify the androiddeployqt JSON file. - To prepare the build for androiddeployqt, it is recommended to build your - project in a separate directory. Run the following commands: + To prepare the environment for androiddeployqt, configure your project in + a separate directory than your source directory. For more information on + configuring your project, see \l {Building Qt for Android Projects from Command Line}. + \section1 Command Line Arguments - \badcode - mkdir build-project - cd build-project - \endcode - - Followed by: - - For qmake: - \badcode - qmake ../project/project.pro - make -j$(nproc) - make -j$(nproc) apk_install_target - \endcode - - For CMake: - \badcode - cmake --build - \endcode + The only required command line arguments when running the tool are + \c {--input} and \c {--output}. Other command line arguments are optional but + useful. The list below is available by passing the \c {--help} argument to + androiddeployqt. - \section1 Command Line Arguments + \quotefromfile main.cpp + \skipto Syntax: androiddeployqt --output <destination> [options] + \printuntil --help: Displays this information. - The only required command line argument when running the tool is \c{--output}. - Other command line arguments are optional but useful. Here's a quick overview. - More information is available by passing the \c{--help} argument to androiddeployqt. - - \table - \header - \li Argument - \li Brief Description - \row - \li \c{--output <destination>} - \li Specifies the destination of the final package. Set this to - \c{$ANDROID_BUILD_DIR}, that is the build directory where you installed - your application binaries. - \row - \li \c{--input <file name>} - \li This allows you to specify the generated \c JSON settings file. - \c androiddeployqt will try to guess the file name based on the - current working directory. - \row - \li \c{--aab} - \li Generate an Android Application Bundle, rather than an APK. Note - that this invalidates some of the other arguments, such as \c{--install}. - \row - \li \c{--deployment <mechanism>} - \li Specify this to pick a different deployment mechanism than the - default. - \list - \li \c Bundled: includes all the app's dependencies inside - the APK. - \li \c Unbundled: excludes native libraries from the APK. - The libraries are expected to be present on the target - device. The location can be provided either by setting - the property - \l{cmake-target-property-QT_ANDROID_SYSTEM_LIBS_PREFIX}{QT_ANDROID_SYSTEM_LIBS_PREFIX} - in your CMake project file, or by defining the path as - meta-data in - \l {Qt Android Manifest File Configuration}{AndroidManifest.xml}: - \badcode - <application> - <meta-data - android:name="system_libs_prefix" - android:value="path/to/libraries/"/> - </application> - \endcode - If no path is provided, \c /system/lib/ is used as the - default path. - \note \c Unbundled deployment does not support - incremental builds. - \note \c Unbundled deployment not supported when - deploying as AAB. - \endlist - Default is \c Bundled deployment. - \row - \li \c{--install} - \li Specify this to install the finished package on the target device - or emulator. Note that if a previous version of the package is - already installed, it will be uninstalled first, removing any - data it might have stored locally. - \row - \li \c{--device <ID>} - \li Specify the ID of the target device or emulator as reported by - the \c adb tool. If an ID is specified, it will be passed to all - calls to \c adb. If it is unspecified, no particular device or - emulator will be requested by \c adb, causing it to pick a default - instead. - \row - \li \c{--android-platform <platform>} - \li The SDK platform used for building the Java code of the application. - By default, the latest available platform is used. - \row - \li \c{--release} - \li Specify this to create a release package instead of a debug package. - With no other arguments, release packages are unsigned and cannot - be installed to any device before they have been signed by a private - key. - \row - \li \c{--sign <url> <alias>} - \li Sign the resulting package. Specifying this also implies - \c{--release}. The URL of the keystore file and the alias of the - key have to be specified. Optionally, set the following environment - variables to conceal the signing information - \c QT_ANDROID_KEYSTORE_PATH, \c QT_ANDROID_KEYSTORE_ALIAS, - \c QT_ANDROID_KEYSTORE_STORE_PASS, and \c QT_ANDROID_KEYSTORE_KEY_PASS. - In addition, there are a number of options that can be specified - which are passed through to the \c jarsigner tool. - Pass \c{--help} to \c androiddeployqt for more information. - \row - \li \c{--jdk <path>} - \li Specify the path to the Java Development Kit. This is only - required for signing packages, as it is only used for finding - the \c jarsigner tool. If it is unspecified, then \c androiddeployqt - will attempt to detect \c jarsigner, either using the \c{JAVA_HOME} - environment variable, or on the \c PATH. - \row - \li \c{--verbose} - \li Specify this to output more information about what \c androiddeployqt is - doing. - \row - \li \c{--help} - \li Prints the help for the tool. - \endtable - - With a project named \c project, to directly build the application package - with \c androiddeployqt without deploying it the device, run the following: + With a \c project_name, to build the application package with \c androiddeployqt + without deploying it the device, run the following: \badcode - .androiddeployqt --input $BUILD_DIR/android-project-deployment-settings.json --output $ANDROID_BUILD_DIR + androiddeployqt --input <build_dir>/android-project_name-deployment-settings.json \ + --output <build_dir>/android-build \endcode - To deploy the built package to the device: + To build and deploy the package to the device: \badcode - androiddeployqt --verbose --output $ANDROID_BUILD_DIR --no-build --input $BUILD_DIR/android-project-deployment-settings.json --gradle --reinstall --device <adb_device_id> + androiddeployqt --input <build_dir>/android-project_name-deployment-settings.json \ + --output <build_dir>/android-build --install --device <device_serial_id> \endcode \section1 Dependencies Detection @@ -170,11 +59,13 @@ dependencies based on the Qt dependencies of your application. If the plugin has any Qt dependencies which are not also dependencies of your application, it will not be included by default. For instance, in order to ensure that - the SVG image format plugin is included, you will need to add \l{Qt SVG} + the SVG image format plugin is included, you will need to add \l {Qt SVG} module to your project for it to become a dependency of your application: \badcode - QT += svg + find_package(Qt6 REQUIRED COMPONENTS Svg) + ... + target_link_libraries(target_name PRIVATE Qt6::Svg) \endcode If you are wondering why a particular plugin is not included automatically, @@ -185,7 +76,7 @@ \uicontrol {Advanced Actions}. It's also possible to manually specify the dependencies of your application. - For more information, see \l{ANDROID_DEPLOYMENT_DEPENDENCIES} qmake variable. + For more information, see \l {QT_ANDROID_DEPLOYMENT_DEPENDENCIES} CMake variable. \note androiddeployqt scans the QML files of the project to collect the QML imports. However, if you are loading QML code as a QString from C++ at runtime, that might @@ -193,62 +84,12 @@ To remedy that, you can add a dummy QML file that imports such QML modules that are referenced at runtime. - \section1 Android-specific qmake Variables - - Unless the project has special requirements such as third party libraries, - it should be possible to run \l androiddeployqt on it with no modifications - and get a working Qt for Android application. - - There are two important environment variables used by Qt: - - \list - \li \c{ANDROID_SDK_ROOT}: specifies the path to the Android SDK used for - building the application. The Android SDK contains the build-tools, - Android NDK, and Android toolchains. - \li \c{ANDROID_NDK_ROOT}: specifies the path to the Android NDK used to - build the application. It is not recommended to hard-code this path, - since different Qt for Android versions can depend on different - Android NDK versions. - \endlist - - \note Qt Creator sets these variables by default. - - There are a set of \c qmake or \c CMake variables that can be used to tailor - your package. At some point during development, you will most likely want - to look into these variables to customize your application. - - Here is a list of some variables that are particularly interesting when - making Android applications: - - \list - \li \l{ANDROID_PACKAGE_SOURCE_DIR} - \li \l{ANDROID_VERSION_CODE} - \li \l{ANDROID_VERSION_NAME} - \li \l{ANDROID_EXTRA_LIBS} - \li \l{ANDROID_EXTRA_PLUGINS} - \li \l{ANDROID_ABIS} - \li \l{ANDROID_API_VERSION} - \li \l{ANDROID_DEPLOYMENT_DEPENDENCIES} - \li \l{ANDROID_MIN_SDK_VERSION} - \li \l{ANDROID_TARGET_SDK_VERSION} - \li \l{JAVA_HOME} - \endlist - - Also, the following \c qmake variables are primarily useful when writing a Qt module, and not - normal applications: - - \list - \li \l{ANDROID_BUNDLED_JAR_DEPENDENCIES} - \li \l{ANDROID_LIB_DEPENDENCIES} - \li \l{ANDROID_PERMISSIONS} - \li \l{ANDROID_FEATURES} - \endlist - - \note This list of variables can also be used with CMake. - \section1 Deployment in Qt Creator - Qt Creator runs \c androiddeployqt by default, and provides easy - and intuitive user interfaces to specify many of the options. For more - information, see \l{Qt Creator: Deploying Applications to Android Devices}. + Qt Creator uses \c androiddeployqt under the hood, and provides easy + and intuitive user interfaces to specify various options. For more information, + see \l{Qt Creator: Deploying Applications to Android Devices}. + + For more information about customizing and deploying a Qt for Android app, + see \l {Deploying an Application on Android}. */ diff --git a/src/tools/androiddeployqt/main.cpp b/src/tools/androiddeployqt/main.cpp index 3db00ad01f..6125b405b5 100644 --- a/src/tools/androiddeployqt/main.cpp +++ b/src/tools/androiddeployqt/main.cpp @@ -13,7 +13,7 @@ #include <QXmlStreamReader> #include <QStandardPaths> #include <QUuid> -#include <QDirIterator> +#include <QDirListing> #include <QElapsedTimer> #include <QRegularExpression> #include <QSettings> @@ -70,10 +70,18 @@ struct QtDependency struct QtInstallDirectoryWithTriple { - QtInstallDirectoryWithTriple(const QString &dir = QString(), const QString &t = QString()) : - qtInstallDirectory(dir), triple(t), enabled(false) {} + QtInstallDirectoryWithTriple(const QString &dir = QString(), + const QString &t = QString(), + const QHash<QString, QString> &dirs = QHash<QString, QString>() + ) : + qtInstallDirectory(dir), + qtDirectories(dirs), + triple(t), + enabled(false) + {} QString qtInstallDirectory; + QHash<QString, QString> qtDirectories; QString triple; bool enabled; }; @@ -93,7 +101,6 @@ struct Options , internalSf(false) , sectionsOnly(false) , protectedAuthenticationPath(false) - , jarSigner(false) , installApk(false) , uninstallApk(false) , qmlImportScannerBinaryPath() @@ -129,8 +136,15 @@ struct Options // Build paths QString qtInstallDirectory; + QHash<QString, QString> qtDirectories; + QString qtDataDirectory; + QString qtLibsDirectory; + QString qtLibExecsDirectory; + QString qtPluginsDirectory; + QString qtQmlDirectory; QString qtHostDirectory; std::vector<QString> extraPrefixDirs; + QStringList androidDeployPlugins; // Unlike 'extraPrefixDirs', the 'extraLibraryDirs' key doesn't expect the 'lib' subfolder // when looking for dependencies. std::vector<QString> extraLibraryDirs; @@ -150,7 +164,7 @@ struct Options QString versionName; QString versionCode; QByteArray minSdkVersion{"23"}; - QByteArray targetSdkVersion{"30"}; + QByteArray targetSdkVersion{"34"}; // lib c++ path QString stdCppPath; @@ -191,7 +205,6 @@ struct Options bool internalSf; bool sectionsOnly; bool protectedAuthenticationPath; - bool jarSigner; QString apkPath; // Installation information @@ -200,12 +213,19 @@ struct Options QString installLocation; // Per architecture collected information - void setCurrentQtArchitecture(const QString &arch, const QString &directory) + void setCurrentQtArchitecture(const QString &arch, + const QString &directory, + const QHash<QString, QString> &directories) { currentArchitecture = arch; qtInstallDirectory = directory; + qtDataDirectory = directories["qtDataDirectory"_L1]; + qtLibsDirectory = directories["qtLibsDirectory"_L1]; + qtLibExecsDirectory = directories["qtLibExecsDirectory"_L1]; + qtPluginsDirectory = directories["qtPluginsDirectory"_L1]; + qtQmlDirectory = directories["qtQmlDirectory"_L1]; } - typedef QPair<QString, QString> BundledFile; + using BundledFile = std::pair<QString, QString>; QHash<QString, QList<BundledFile>> bundledFiles; QHash<QString, QList<QtDependency>> qtDependencies; QHash<QString, QStringList> localLibs; @@ -218,6 +238,7 @@ struct Options // Override qml import scanner path QString qmlImportScannerBinaryPath; + bool qmlSkipImportScanning = false; }; static const QHash<QByteArray, QByteArray> elfArchitectures = { @@ -227,6 +248,12 @@ static const QHash<QByteArray, QByteArray> elfArchitectures = { {"x86_64", "x86_64"} }; +bool goodToCopy(const Options *options, const QString &file, QStringList *unmetDependencies); +bool checkCanImportFromRootPaths(const Options *options, const QString &absolutePath, + const QString &moduleUrl); +bool readDependenciesFromElf(Options *options, const QString &fileName, + QSet<QString> *usedDependencies, QSet<QString> *remainingDependencies); + QString architectureFromName(const QString &name) { QRegularExpression architecture(QStringLiteral("_(armeabi-v7a|arm64-v8a|x86|x86_64).so$")); @@ -292,7 +319,6 @@ QString fileArchitecture(const Options &options, const QString &path) char buffer[512]; while (fgets(buffer, sizeof(buffer), readElfCommand) != nullptr) { QByteArray line = QByteArray::fromRawData(buffer, qstrlen(buffer)); - QString library; line = line.trimmed(); if (line.startsWith("Arch: ")) { auto it = elfArchitectures.find(line.mid(6)); @@ -321,7 +347,7 @@ void deleteMissingFiles(const Options &options, const QDir &srcDir, const QDir & for (const QFileInfo &src : srcEntries) if (dst.fileName() == src.fileName()) { if (dst.isDir()) - deleteMissingFiles(options, src.absoluteDir(), dst.absoluteDir()); + deleteMissingFiles(options, src.absoluteFilePath(), dst.absoluteFilePath()); found = true; break; } @@ -359,7 +385,6 @@ Options parseOptions() } else if (argument.compare("--aab"_L1, Qt::CaseInsensitive) == 0) { options.buildAAB = true; options.build = true; - options.jarSigner = true; } else if (!options.buildAAB && argument.compare("--no-build"_L1, Qt::CaseInsensitive) == 0) { options.build = false; } else if (argument.compare("--install"_L1, Qt::CaseInsensitive) == 0) { @@ -425,6 +450,7 @@ Options parseOptions() const QString storeAlias = qEnvironmentVariable("QT_ANDROID_KEYSTORE_ALIAS"); if (keyStore.isEmpty() || storeAlias.isEmpty()) { options.helpRequested = true; + fprintf(stderr, "Package signing path and alias values are not specified.\n"); } else { fprintf(stdout, "Using package signing path and alias values found from the " @@ -432,9 +458,14 @@ Options parseOptions() options.keyStore = keyStore; options.keyStoreAlias = storeAlias; } - } else { + } else if (!arguments.at(i + 1).startsWith("--"_L1) && + !arguments.at(i + 2).startsWith("--"_L1)) { options.keyStore = arguments.at(++i); options.keyStoreAlias = arguments.at(++i); + } else { + options.helpRequested = true; + fprintf(stderr, "Package signing path and alias values are not " + "specified.\n"); } // Do not override if the passwords are provided through arguments @@ -494,8 +525,6 @@ Options parseOptions() options.sectionsOnly = true; } else if (argument.compare("--protected"_L1, Qt::CaseInsensitive) == 0) { options.protectedAuthenticationPath = true; - } else if (argument.compare("--jarsigner"_L1, Qt::CaseInsensitive) == 0) { - options.jarSigner = true; } else if (argument.compare("--aux-mode"_L1, Qt::CaseInsensitive) == 0) { options.auxMode = true; } else if (argument.compare("--qml-importscanner-binary"_L1, Qt::CaseInsensitive) == 0) { @@ -531,113 +560,112 @@ Options parseOptions() void printHelp() { - fprintf(stderr, "Syntax: %s --output <destination> [options]\n" - "\n" - " Creates an Android package in the build directory <destination> and\n" - " builds it into an .apk file.\n" - "\n" - " Optional arguments:\n" - " --input <inputfile>: Reads <inputfile> for options generated by\n" - " qmake. A default file name based on the current working\n" - " directory will be used if nothing else is specified.\n" - "\n" - " --deployment <mechanism>: Supported deployment mechanisms:\n" - " bundled (default): Includes Qt files in stand-alone package.\n" - " unbundled: Assumes native libraries are present on the device\n" - " and does not include them in the APK.\n" - "\n" - " --aab: Build an Android App Bundle.\n" - "\n" - " --no-build: Do not build the package, it is useful to just install\n" - " a package previously built.\n" - "\n" - " --install: Installs apk to device/emulator. By default this step is\n" - " not taken. If the application has previously been installed on\n" - " the device, it will be uninstalled first.\n" - "\n" - " --reinstall: Installs apk to device/emulator. By default this step\n" - " is not taken. If the application has previously been installed on\n" - " the device, it will be overwritten, but its data will be left\n" - " intact.\n" - "\n" - " --device [device ID]: Use specified device for deployment. Default\n" - " is the device selected by default by adb.\n" - "\n" - " --android-platform <platform>: Builds against the given android\n" - " platform. By default, the highest available version will be\n" - " used.\n" - "\n" - " --release: Builds a package ready for release. By default, the\n" - " package will be signed with a debug key.\n" - "\n" - " --sign <url/to/keystore> <alias>: Signs the package with the\n" - " specified keystore, alias and store password.\n" - " Optional arguments for use with signing:\n" - " --storepass <password>: Keystore password.\n" - " --storetype <type>: Keystore type.\n" - " --keypass <password>: Password for private key (if different\n" - " from keystore password.)\n" - " --sigfile <file>: Name of .SF/.DSA file.\n" - " --digestalg <name>: Name of digest algorithm. Default is\n" - " \"SHA1\".\n" - " --sigalg <name>: Name of signature algorithm. Default is\n" - " \"SHA1withRSA\".\n" - " --tsa <url>: Location of the Time Stamping Authority.\n" - " --tsacert <alias>: Public key certificate for TSA.\n" - " --internalsf: Include the .SF file inside the signature block.\n" - " --sectionsonly: Don't compute hash of entire manifest.\n" - " --protected: Keystore has protected authentication path.\n" - " --jarsigner: Force jarsigner usage, otherwise apksigner will be\n" - " used if available.\n" - "\n" - " NOTE: To conceal the keystore information, the environment variables\n" - " QT_ANDROID_KEYSTORE_PATH, and QT_ANDROID_KEYSTORE_ALIAS are used to\n" - " set the values keysotore and alias respectively.\n" - " Also the environment variables QT_ANDROID_KEYSTORE_STORE_PASS,\n" - " and QT_ANDROID_KEYSTORE_KEY_PASS are used to set the store and key\n" - " passwords respectively. This option needs only the --sign parameter.\n" - "\n" - " --jdk <path/to/jdk>: Used to find the jarsigner tool when used\n" - " in combination with the --release argument. By default,\n" - " an attempt is made to detect the tool using the JAVA_HOME and\n" - " PATH environment variables, in that order.\n" - "\n" - " --qml-import-paths: Specify additional search paths for QML\n" - " imports.\n" - "\n" - " --verbose: Prints out information during processing.\n" - "\n" - " --no-generated-assets-cache: Do not pregenerate the entry list for\n" - " the assets file engine.\n" - "\n" - " --aux-mode: Operate in auxiliary mode. This will only copy the\n" - " dependencies into the build directory and update the XML templates.\n" - " The project will not be built or installed.\n" - "\n" - " --apk <path/where/to/copy/the/apk>: Path where to copy the built apk.\n" - "\n" - " --qml-importscanner-binary <path/to/qmlimportscanner>: Override the\n" - " default qmlimportscanner binary path. By default the\n" - " qmlimportscanner binary is located using the Qt directory\n" - " specified in the input file.\n" - "\n" - " --depfile <path/to/depfile>: Output a dependency file.\n" - "\n" - " --builddir <path/to/build/directory>: build directory. Necessary when\n" - " generating a depfile because ninja requires relative paths.\n" - "\n" - " --no-rcc-bundle-cleanup: skip cleaning rcc bundle directory after\n" - " running androiddeployqt. This option simplifies debugging of\n" - " the resource bundle content, but it should not be used when deploying\n" - " a project, since it litters the 'assets' directory.\n" - "\n" - " --copy-dependencies-only: resolve application dependencies and stop\n" - " deploying process after all libraries and resources that the\n" - " application depends on have been copied.\n" - "\n" - " --help: Displays this information.\n", - qPrintable(QCoreApplication::arguments().at(0)) - ); + fprintf(stderr, R"( +Syntax: androiddeployqt --output <destination> [options] + +Creates an Android package in the build directory <destination> and +builds it into an .apk file. + +Optional arguments: + --input <inputfile>: Reads <inputfile> for options generated by + qmake. A default file name based on the current working + directory will be used if nothing else is specified. + + --deployment <mechanism>: Supported deployment mechanisms: + bundled (default): Includes Qt files in stand-alone package. + unbundled: Assumes native libraries are present on the device + and does not include them in the APK. + + --aab: Build an Android App Bundle. + + --no-build: Do not build the package, it is useful to just install + a package previously built. + + --install: Installs apk to device/emulator. By default this step is + not taken. If the application has previously been installed on + the device, it will be uninstalled first. + + --reinstall: Installs apk to device/emulator. By default this step + is not taken. If the application has previously been installed on + the device, it will be overwritten, but its data will be left + intact. + + --device [device ID]: Use specified device for deployment. Default + is the device selected by default by adb. + + --android-platform <platform>: Builds against the given android + platform. By default, the highest available version will be + used. + + --release: Builds a package ready for release. By default, the + package will be signed with a debug key. + + --sign <url/to/keystore> <alias>: Signs the package with the + specified keystore, alias and store password. + Optional arguments for use with signing: + --storepass <password>: Keystore password. + --storetype <type>: Keystore type. + --keypass <password>: Password for private key (if different + from keystore password.) + --sigfile <file>: Name of .SF/.DSA file. + --digestalg <name>: Name of digest algorithm. Default is + "SHA-256". + --sigalg <name>: Name of signature algorithm. Default is + "SHA256withRSA". + --tsa <url>: Location of the Time Stamping Authority. + --tsacert <alias>: Public key certificate for TSA. + --internalsf: Include the .SF file inside the signature block. + --sectionsonly: Don't compute hash of entire manifest. + --protected: Keystore has protected authentication path. + --jarsigner: Deprecated, ignored. + + NOTE: To conceal the keystore information, the environment variables + QT_ANDROID_KEYSTORE_PATH, and QT_ANDROID_KEYSTORE_ALIAS are used to + set the values keysotore and alias respectively. + Also the environment variables QT_ANDROID_KEYSTORE_STORE_PASS, + and QT_ANDROID_KEYSTORE_KEY_PASS are used to set the store and key + passwords respectively. This option needs only the --sign parameter. + + --jdk <path/to/jdk>: Used to find the jarsigner tool when used + in combination with the --release argument. By default, + an attempt is made to detect the tool using the JAVA_HOME and + PATH environment variables, in that order. + + --qml-import-paths: Specify additional search paths for QML + imports. + + --verbose: Prints out information during processing. + + --no-generated-assets-cache: Do not pregenerate the entry list for + the assets file engine. + + --aux-mode: Operate in auxiliary mode. This will only copy the + dependencies into the build directory and update the XML templates. + The project will not be built or installed. + + --apk <path/where/to/copy/the/apk>: Path where to copy the built apk. + + --qml-importscanner-binary <path/to/qmlimportscanner>: Override the + default qmlimportscanner binary path. By default the + qmlimportscanner binary is located using the Qt directory + specified in the input file. + + --depfile <path/to/depfile>: Output a dependency file. + + --builddir <path/to/build/directory>: build directory. Necessary when + generating a depfile because ninja requires relative paths. + + --no-rcc-bundle-cleanup: skip cleaning rcc bundle directory after + running androiddeployqt. This option simplifies debugging of + the resource bundle content, but it should not be used when deploying + a project, since it litters the 'assets' directory. + + --copy-dependencies-only: resolve application dependencies and stop + deploying process after all libraries and resources that the + application depends on have been copied. + + --help: Displays this information. +)"); } // Since strings compared will all start with the same letters, @@ -648,10 +676,10 @@ bool quasiLexicographicalReverseLessThan(const QFileInfo &fi1, const QFileInfo & QString s1 = fi1.baseName(); QString s2 = fi2.baseName(); - if (s1.length() == s2.length()) + if (s1.size() == s2.size()) return s1 > s2; else - return s1.length() > s2.length(); + return s1.size() > s2.size(); } // Files which contain templates that need to be overwritten by build data should be overwritten every @@ -741,10 +769,10 @@ QString cleanPackageName(QString packageName) // No keywords qsizetype index = -1; - while (index < packageName.length()) { + while (index < packageName.size()) { qsizetype next = packageName.indexOf(u'.', index + 1); if (next == -1) - next = packageName.length(); + next = packageName.size(); QString word = packageName.mid(index + 1, next - index - 1); if (!word.isEmpty()) { QChar c = word[0]; @@ -781,7 +809,7 @@ QString detectLatestAndroidPlatform(const QString &sdkPath) std::sort(fileInfos.begin(), fileInfos.end(), quasiLexicographicalReverseLessThan); - QFileInfo latestPlatform = fileInfos.first(); + const QFileInfo& latestPlatform = fileInfos.constFirst(); return latestPlatform.baseName(); } @@ -809,6 +837,66 @@ bool parseCmakeBoolean(const QJsonValue &value) || stringValue.toInt() > 0); } +bool readInputFileDirectory(Options *options, QJsonObject &jsonObject, const QString keyName) +{ + const QJsonValue qtDirectory = jsonObject.value(keyName); + if (qtDirectory.isUndefined()) { + for (auto it = options->architectures.constBegin(); it != options->architectures.constEnd(); ++it) { + if (keyName == "qtDataDirectory"_L1) { + options->architectures[it.key()].qtDirectories[keyName] = "."_L1; + break; + } else if (keyName == "qtLibsDirectory"_L1) { + options->architectures[it.key()].qtDirectories[keyName] = "lib"_L1; + break; + } else if (keyName == "qtLibExecsDirectory"_L1) { + options->architectures[it.key()].qtDirectories[keyName] = defaultLibexecDir(); + break; + } else if (keyName == "qtPluginsDirectory"_L1) { + options->architectures[it.key()].qtDirectories[keyName] = "plugins"_L1; + break; + } else if (keyName == "qtQmlDirectory"_L1) { + options->architectures[it.key()].qtDirectories[keyName] = "qml"_L1; + break; + } + } + return true; + } + + if (qtDirectory.isObject()) { + const QJsonObject object = qtDirectory.toObject(); + for (auto it = object.constBegin(); it != object.constEnd(); ++it) { + if (it.value().isUndefined()) { + fprintf(stderr, + "Invalid '%s' record in deployment settings: %s\n", + qPrintable(keyName), + qPrintable(it.value().toString())); + return false; + } + if (it.value().isNull()) + continue; + if (!options->architectures.contains(it.key())) { + fprintf(stderr, "Architecture %s unknown (%s).", qPrintable(it.key()), + qPrintable(options->architectures.keys().join(u','))); + return false; + } + options->architectures[it.key()].qtDirectories[keyName] = it.value().toString(); + } + } else if (qtDirectory.isString()) { + // Format for Qt < 6 or when using the tool with Qt >= 6 but in single arch. + // We assume Qt > 5.14 where all architectures are in the same directory. + const QString directory = qtDirectory.toString(); + options->architectures["arm64-v8a"_L1].qtDirectories[keyName] = directory; + options->architectures["armeabi-v7a"_L1].qtDirectories[keyName] = directory; + options->architectures["x86"_L1].qtDirectories[keyName] = directory; + options->architectures["x86_64"_L1].qtDirectories[keyName] = directory; + } else { + fprintf(stderr, "Invalid format for %s in json file %s.\n", + qPrintable(keyName), qPrintable(options->inputFileName)); + return false; + } + return true; +} + bool readInputFile(Options *options) { QFile file(options->inputFileName); @@ -865,7 +953,8 @@ bool readInputFile(Options *options) const QJsonObject object = qtInstallDirectory.toObject(); for (auto it = object.constBegin(); it != object.constEnd(); ++it) { if (it.value().isUndefined()) { - fprintf(stderr, "Invalid architecture: %s\n", + fprintf(stderr, + "Invalid 'qt' record in deployment settings: %s\n", qPrintable(it.value().toString())); return false; } @@ -893,6 +982,14 @@ bool readInputFile(Options *options) return false; } } + + if (!readInputFileDirectory(options, jsonObject, "qtDataDirectory"_L1) || + !readInputFileDirectory(options, jsonObject, "qtLibsDirectory"_L1) || + !readInputFileDirectory(options, jsonObject, "qtLibExecsDirectory"_L1) || + !readInputFileDirectory(options, jsonObject, "qtPluginsDirectory"_L1) || + !readInputFileDirectory(options, jsonObject, "qtQmlDirectory"_L1)) + return false; + { const QJsonValue qtHostDirectory = jsonObject.value("qtHostDir"_L1); if (!qtHostDirectory.isUndefined()) { @@ -915,6 +1012,11 @@ bool readInputFile(Options *options) } { + const auto androidDeployPlugins = jsonObject.value("android-deploy-plugins"_L1).toString(); + options->androidDeployPlugins = androidDeployPlugins.split(";"_L1, Qt::SkipEmptyParts); + } + + { const auto extraLibraryDirs = jsonObject.value("extraLibraryDirs"_L1).toArray(); options->extraLibraryDirs.reserve(extraLibraryDirs.size()); for (const QJsonValue path : extraLibraryDirs) { @@ -1030,6 +1132,12 @@ bool readInputFile(Options *options) } { + const QJsonValue qmlSkipImportScanning = jsonObject.value("qml-skip-import-scanning"_L1); + if (!qmlSkipImportScanning.isUndefined()) + options->qmlSkipImportScanning = qmlSkipImportScanning.toBool(); + } + + { const QJsonValue extraPlugins = jsonObject.value("android-extra-plugins"_L1); if (!extraPlugins.isUndefined()) options->extraPlugins = extraPlugins.toString().split(u','); @@ -1071,7 +1179,7 @@ bool readInputFile(Options *options) options->rootPaths.push_back(path.toString()); } } else { - options->rootPaths.push_back(options->inputFileName); + options->rootPaths.push_back(QFileInfo(options->inputFileName).absolutePath()); } } @@ -1114,6 +1222,7 @@ bool readInputFile(Options *options) } { + using ItFlag = QDirListing::IteratorFlag; const QJsonValue deploymentDependencies = jsonObject.value("deployment-dependencies"_L1); if (!deploymentDependencies.isUndefined()) { QString deploymentDependenciesString = deploymentDependencies.toString(); @@ -1122,14 +1231,12 @@ bool readInputFile(Options *options) QString path = options->qtInstallDirectory + QChar::fromLatin1('/'); path += dependency; if (QFileInfo(path).isDir()) { - QDirIterator iterator(path, QDirIterator::Subdirectories); - while (iterator.hasNext()) { - iterator.next(); - if (iterator.fileInfo().isFile()) { - QString subPath = iterator.filePath(); + for (const auto &dirEntry : QDirListing(path, ItFlag::Recursive)) { + if (dirEntry.isFile()) { + const QString subPath = dirEntry.filePath(); auto arch = fileArchitecture(*options, subPath); if (!arch.isEmpty()) { - options->qtDependencies[arch].append(QtDependency(subPath.mid(options->qtInstallDirectory.length() + 1), + options->qtDependencies[arch].append(QtDependency(subPath.mid(options->qtInstallDirectory.size() + 1), subPath)); } else if (options->verbose) { fprintf(stderr, "Skipping \"%s\", unknown architecture\n", qPrintable(subPath)); @@ -1138,12 +1245,24 @@ bool readInputFile(Options *options) } } } else { - auto arch = fileArchitecture(*options, path); - if (!arch.isEmpty()) { - options->qtDependencies[arch].append(QtDependency(dependency.toString(), path)); - } else if (options->verbose) { - fprintf(stderr, "Skipping \"%s\", unknown architecture\n", qPrintable(path)); - fflush(stderr); + auto qtDependency = [options](const QStringView &dependency, + const QString &arch) { + const auto installDir = options->architectures[arch].qtInstallDirectory; + const auto absolutePath = "%1/%2"_L1.arg(installDir, dependency.toString()); + return QtDependency(dependency.toString(), absolutePath); + }; + + if (dependency.endsWith(QLatin1String(".so"))) { + auto arch = fileArchitecture(*options, path); + if (!arch.isEmpty()) { + options->qtDependencies[arch].append(qtDependency(dependency, arch)); + } else if (options->verbose) { + fprintf(stderr, "Skipping \"%s\", unknown architecture\n", qPrintable(path)); + fflush(stderr); + } + } else { + for (auto arch : options->architectures.keys()) + options->qtDependencies[arch].append(qtDependency(dependency, arch)); } } } @@ -1199,7 +1318,7 @@ void cleanTopFolders(const Options &options, const QDir &srcDir, const QString & const auto dirs = srcDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Dirs); for (const QFileInfo &dir : dirs) { if (dir.fileName() != "libs"_L1) - deleteMissingFiles(options, dir.absoluteDir(), QDir(dstDir + dir.fileName())); + deleteMissingFiles(options, dir.absoluteFilePath(), QDir(dstDir + dir.fileName())); } } @@ -1208,13 +1327,15 @@ void cleanAndroidFiles(const Options &options) if (!options.androidSourceDirectory.isEmpty()) cleanTopFolders(options, QDir(options.androidSourceDirectory), options.outputDirectory); - cleanTopFolders(options, QDir(options.qtInstallDirectory + "/src/android/templates"_L1), + cleanTopFolders(options, + QDir(options.qtInstallDirectory + u'/' + + options.qtDataDirectory + "/src/android/templates"_L1), options.outputDirectory); } bool copyAndroidTemplate(const Options &options, const QString &androidTemplate, const QString &outDirPrefix = QString()) { - QDir sourceDirectory(options.qtInstallDirectory + androidTemplate); + QDir sourceDirectory(options.qtInstallDirectory + u'/' + options.qtDataDirectory + androidTemplate); if (!sourceDirectory.exists()) { fprintf(stderr, "Cannot find template directory %s\n", qPrintable(sourceDirectory.absolutePath())); return false; @@ -1232,7 +1353,8 @@ bool copyAndroidTemplate(const Options &options, const QString &androidTemplate, bool copyGradleTemplate(const Options &options) { - QDir sourceDirectory(options.qtInstallDirectory + "/src/3rdparty/gradle"_L1); + QDir sourceDirectory(options.qtInstallDirectory + u'/' + + options.qtDataDirectory + "/src/3rdparty/gradle"_L1); if (!sourceDirectory.exists()) { fprintf(stderr, "Cannot find template directory %s\n", qPrintable(sourceDirectory.absolutePath())); return false; @@ -1357,7 +1479,8 @@ bool copyAndroidExtraResources(Options *options) } QDir resourceDir(extraResource); - QString assetsDir = options->outputDirectory + "/assets/"_L1 + resourceDir.dirName() + u'/'; + QString assetsDir = options->outputDirectory + "/assets/"_L1 + + resourceDir.dirName() + u'/'; QString libsDir = options->outputDirectory + "/libs/"_L1 + options->currentArchitecture + u'/'; const QStringList files = allFilesInside(resourceDir, resourceDir); @@ -1403,7 +1526,7 @@ bool updateFile(const QString &fileName, const QHash<QString, QString> &replacem forever { int index = contents.indexOf(it.key().toUtf8()); if (index >= 0) { - contents.replace(index, it.key().length(), it.value().toUtf8()); + contents.replace(index, it.key().size(), it.value().toUtf8()); hasReplacements = true; } else { break; @@ -1444,7 +1567,6 @@ bool updateLibsXml(Options *options) for (auto it = options->architectures.constBegin(); it != options->architectures.constEnd(); ++it) { if (!it->enabled) continue; - QString libsPath = "libs/"_L1 + it.key() + u'/'; qtLibs += " <item>%1;%2</item>\n"_L1.arg(it.key(), options->stdCppName); for (const Options::BundledFile &bundledFile : options->bundledFiles[it.key()]) { @@ -1486,27 +1608,21 @@ bool updateLibsXml(Options *options) if (localLibs.isEmpty()) { QString plugin; for (const QtDependency &qtDependency : options->qtDependencies[it.key()]) { - if (qtDependency.relativePath.endsWith("libqtforandroid.so"_L1) - || qtDependency.relativePath.endsWith("libqtforandroidGL.so"_L1)) { - if (!plugin.isEmpty() && plugin != qtDependency.relativePath) { - fprintf(stderr, "Both platform plugins libqtforandroid.so and libqtforandroidGL.so included in package. Please include only one.\n"); - return false; - } - + if (qtDependency.relativePath.contains("libplugins_platforms_qtforandroid_"_L1)) plugin = qtDependency.relativePath; - } + if (qtDependency.relativePath.contains( QString::asprintf("libQt%dOpenGL", QT_VERSION_MAJOR)) || qtDependency.relativePath.contains( QString::asprintf("libQt%dQuick", QT_VERSION_MAJOR))) { options->usesOpenGL |= true; - break; } } if (plugin.isEmpty()) { fflush(stdout); - fprintf(stderr, "No platform plugin, neither libqtforandroid.so or libqtforandroidGL.so, included in package. Please include one.\n"); + fprintf(stderr, "No platform plugin (libplugins_platforms_qtforandroid.so) included" + " in the deployment. Make sure the app links to Qt Gui library.\n"); fflush(stderr); return false; } @@ -1589,12 +1705,12 @@ bool updateAndroidManifest(Options &options) replacements[QStringLiteral("package=\"org.qtproject.example\"")] = "package=\"%1\""_L1.arg(options.packageName); QString permissions; - for (const QString &permission : qAsConst(options.permissions)) + for (const QString &permission : std::as_const(options.permissions)) permissions += " <uses-permission android:name=\"%1\" />\n"_L1.arg(permission); replacements[QStringLiteral("<!-- %%INSERT_PERMISSIONS -->")] = permissions.trimmed(); QString features; - for (const QString &feature : qAsConst(options.features)) + for (const QString &feature : std::as_const(options.features)) features += " <uses-feature android:name=\"%1\" android:required=\"false\" />\n"_L1.arg(feature); if (options.usesOpenGL) features += " <uses-feature android:glEsVersion=\"0x00020000\" android:required=\"true\" />"_L1; @@ -1695,6 +1811,26 @@ static QString absoluteFilePath(const Options *options, const QString &relativeF if (QFile::exists(path)) return path; } + + if (relativeFileName.endsWith("-android-dependencies.xml"_L1)) { + for (const auto &dir : options->extraLibraryDirs) { + const QString path = dir + u'/' + relativeFileName; + if (QFile::exists(path)) + return path; + } + return options->qtInstallDirectory + u'/' + options->qtLibsDirectory + + u'/' + relativeFileName; + } + + if (relativeFileName.startsWith("jar/"_L1)) { + return options->qtInstallDirectory + u'/' + options->qtDataDirectory + + u'/' + relativeFileName; + } + + if (relativeFileName.startsWith("lib/"_L1)) { + return options->qtInstallDirectory + u'/' + options->qtLibsDirectory + + u'/' + relativeFileName.mid(sizeof("lib/") - 1); + } return options->qtInstallDirectory + u'/' + relativeFileName; } @@ -1717,19 +1853,63 @@ QList<QtDependency> findFilesRecursively(const Options &options, const QFileInfo return ret; } else { - return QList<QtDependency>() << QtDependency(info.absoluteFilePath().mid(rootPath.length()), info.absoluteFilePath()); + return QList<QtDependency>() << QtDependency(info.absoluteFilePath().mid(rootPath.size()), info.absoluteFilePath()); } } QList<QtDependency> findFilesRecursively(const Options &options, const QString &fileName) { + // We try to find the fileName in extraPrefixDirs first. The function behaves differently + // depending on what the fileName points to. If fileName is a file then we try to find the + // first occurrence in extraPrefixDirs and return this file. If fileName is directory function + // iterates over it and looks for deployment artifacts in each 'extraPrefixDirs' entry. + // Also we assume that if the fileName is recognized as a directory once it will be directory + // for every 'extraPrefixDirs' entry. + QList<QtDependency> deps; for (const auto &prefix : options.extraPrefixDirs) { QFileInfo info(prefix + u'/' + fileName); - if (info.exists()) - return findFilesRecursively(options, info, prefix + u'/'); + if (info.exists()) { + if (info.isDir()) + deps.append(findFilesRecursively(options, info, prefix + u'/')); + else + return findFilesRecursively(options, info, prefix + u'/'); + } + } + + // Usually android deployment settings contain Qt install directory in extraPrefixDirs. + if (std::find(options.extraPrefixDirs.begin(), options.extraPrefixDirs.end(), + options.qtInstallDirectory) == options.extraPrefixDirs.end()) { + QFileInfo info(options.qtInstallDirectory + "/"_L1 + fileName); + QFileInfo rootPath(options.qtInstallDirectory + "/"_L1); + deps.append(findFilesRecursively(options, info, rootPath.absolutePath())); + } + return deps; +} + +void readDependenciesFromFiles(Options *options, const QList<QtDependency> &files, + QSet<QString> &usedDependencies, + QSet<QString> &remainingDependencies) +{ + for (const QtDependency &fileName : files) { + if (usedDependencies.contains(fileName.absolutePath)) + continue; + + if (fileName.absolutePath.endsWith(".so"_L1)) { + if (!readDependenciesFromElf(options, fileName.absolutePath, &usedDependencies, + &remainingDependencies)) { + fprintf(stdout, "Skipping file dependency: %s\n", + qPrintable(fileName.relativePath)); + continue; + } + } + usedDependencies.insert(fileName.absolutePath); + + if (options->verbose) { + fprintf(stdout, "Appending file dependency: %s\n", qPrintable(fileName.relativePath)); + } + + options->qtDependencies[options->currentArchitecture].append(fileName); } - QFileInfo info(options.qtInstallDirectory + u'/' + fileName); - return findFilesRecursively(options, info, options.qtInstallDirectory + u'/'); } bool readAndroidDependencyXml(Options *options, @@ -1737,7 +1917,7 @@ bool readAndroidDependencyXml(Options *options, QSet<QString> *usedDependencies, QSet<QString> *remainingDependencies) { - QString androidDependencyName = absoluteFilePath(options, "/lib/%1-android-dependencies.xml"_L1.arg(moduleName)); + QString androidDependencyName = absoluteFilePath(options, "%1-android-dependencies.xml"_L1.arg(moduleName)); QFile androidDependencyFile(androidDependencyName); if (androidDependencyFile.exists()) { @@ -1762,23 +1942,15 @@ bool readAndroidDependencyXml(Options *options, QString file = reader.attributes().value("file"_L1).toString(); - // Special case, since this is handled by qmlimportscanner instead - if (!options->rootPaths.empty() - && (file == "qml"_L1 || file == "qml/"_L1)) + if (reader.attributes().hasAttribute("type"_L1) + && reader.attributes().value("type"_L1) == "plugin_dir"_L1 + && !options->androidDeployPlugins.isEmpty()) { continue; + } const QList<QtDependency> fileNames = findFilesRecursively(*options, file); - for (const QtDependency &fileName : fileNames) { - if (usedDependencies->contains(fileName.absolutePath)) - continue; - - usedDependencies->insert(fileName.absolutePath); - - if (options->verbose) - fprintf(stdout, "Appending dependency from xml: %s\n", qPrintable(fileName.relativePath)); - - options->qtDependencies[options->currentArchitecture].append(fileName); - } + readDependenciesFromFiles(options, fileNames, *usedDependencies, + *remainingDependencies); } else if (reader.name() == "jar"_L1) { int bundling = reader.attributes().value("bundling"_L1).toInt(); QString fileName = QDir::cleanPath(reader.attributes().value("file"_L1).toString()); @@ -1862,6 +2034,7 @@ QStringList getQtLibsFromElf(const Options &options, const QString &fileName) if (it == elfArchitectures.constEnd() || *it != options.currentArchitecture.toLatin1()) { if (options.verbose) fprintf(stdout, "Skipping \"%s\", architecture mismatch\n", qPrintable(fileName)); + pclose(readElfCommand); return {}; } } @@ -1915,7 +2088,7 @@ bool readDependenciesFromElf(Options *options, dependenciesToCheck.append(dependency); } - for (const QString &dependency : qAsConst(dependenciesToCheck)) { + for (const QString &dependency : std::as_const(dependenciesToCheck)) { QString qtBaseName = dependency.mid(sizeof("lib/lib") - 1); qtBaseName = qtBaseName.left(qtBaseName.size() - (sizeof(".so") - 1)); if (!readAndroidDependencyXml(options, qtBaseName, usedDependencies, remainingDependencies)) { @@ -1926,10 +2099,6 @@ bool readDependenciesFromElf(Options *options, return true; } -bool goodToCopy(const Options *options, const QString &file, QStringList *unmetDependencies); -bool checkCanImportFromRootPaths(const Options *options, const QString &absolutePath, - const QUrl &moduleUrl); - bool scanImports(Options *options, QSet<QString> *usedDependencies) { if (options->verbose) @@ -1939,8 +2108,8 @@ bool scanImports(Options *options, QSet<QString> *usedDependencies) if (!options->qmlImportScannerBinaryPath.isEmpty()) { qmlImportScanner = options->qmlImportScannerBinaryPath; } else { - qmlImportScanner = execSuffixAppended(options->qtInstallDirectory + u'/' - + defaultLibexecDir() + "/qmlimportscanner"_L1); + qmlImportScanner = execSuffixAppended(options->qtLibExecsDirectory + + "/qmlimportscanner"_L1); } QStringList importPaths; @@ -1949,7 +2118,7 @@ bool scanImports(Options *options, QSet<QString> *usedDependencies) // lacks a qml directory. We don't want to pass it as an import path if it doesn't exist // because it will cause qmlimportscanner to fail. // This also covers the case when only qtbase is installed in a regular Qt build. - const QString mainImportPath = options->qtInstallDirectory + "/qml"_L1; + const QString mainImportPath = options->qtInstallDirectory + u'/' + options->qtQmlDirectory; if (QFile::exists(mainImportPath)) importPaths += shellQuote(mainImportPath); @@ -1960,7 +2129,7 @@ bool scanImports(Options *options, QSet<QString> *usedDependencies) importPaths += shellQuote(prefix + "/qml"_L1); // These are provided by both CMake and qmake. - for (const QString &qmlImportPath : qAsConst(options->qmlImportPaths)) { + for (const QString &qmlImportPath : std::as_const(options->qmlImportPaths)) { if (QFile::exists(qmlImportPath)) { importPaths += shellQuote(qmlImportPath); } else { @@ -2033,6 +2202,7 @@ bool scanImports(Options *options, QSet<QString> *usedDependencies) QJsonDocument jsonDocument = QJsonDocument::fromJson(output); if (jsonDocument.isNull()) { fprintf(stderr, "Invalid json output from qmlimportscanner.\n"); + pclose(qmlImportScannerCommand); return false; } @@ -2041,6 +2211,7 @@ bool scanImports(Options *options, QSet<QString> *usedDependencies) QJsonValue value = jsonArray.at(i); if (!value.isObject()) { fprintf(stderr, "Invalid format of qmlimportscanner output.\n"); + pclose(qmlImportScannerCommand); return false; } @@ -2068,21 +2239,17 @@ bool scanImports(Options *options, QSet<QString> *usedDependencies) const QUrl url(object.value("name"_L1).toString()); - if (checkCanImportFromRootPaths(options, info.absolutePath(), url)) { + const QString moduleUrlPath = u"/"_s + url.toString().replace(u'.', u'/'); + if (checkCanImportFromRootPaths(options, info.absolutePath(), moduleUrlPath)) { if (options->verbose) fprintf(stdout, " -- Skipping because path is in QML root path.\n"); continue; } QString importPathOfThisImport; - for (const QString &importPath : qAsConst(importPaths)) { -#if defined(Q_OS_WIN32) - Qt::CaseSensitivity caseSensitivity = Qt::CaseInsensitive; -#else - Qt::CaseSensitivity caseSensitivity = Qt::CaseSensitive; -#endif + for (const QString &importPath : std::as_const(importPaths)) { QString cleanImportPath = QDir::cleanPath(importPath); - if (info.absoluteFilePath().startsWith(cleanImportPath, caseSensitivity)) { + if (QFile::exists(cleanImportPath + moduleUrlPath)) { importPathOfThisImport = importPath; break; } @@ -2090,6 +2257,7 @@ bool scanImports(Options *options, QSet<QString> *usedDependencies) if (importPathOfThisImport.isEmpty()) { fprintf(stderr, "Import found outside of import paths: %s.\n", qPrintable(info.absoluteFilePath())); + pclose(qmlImportScannerCommand); return false; } @@ -2157,15 +2325,15 @@ bool scanImports(Options *options, QSet<QString> *usedDependencies) } } + pclose(qmlImportScannerCommand); return true; } bool checkCanImportFromRootPaths(const Options *options, const QString &absolutePath, - const QUrl &moduleUrl) + const QString &moduleUrlPath) { - const QString pathFromUrl = u"/"_s + moduleUrl.toString().replace(u'.', u'/'); for (auto rootPath : options->rootPaths) { - if ((rootPath + pathFromUrl) == absolutePath) + if ((rootPath + moduleUrlPath) == absolutePath) return true; } return false; @@ -2208,8 +2376,7 @@ bool createRcc(const Options &options) if (!options.rccBinaryPath.isEmpty()) { rcc = options.rccBinaryPath; } else { - rcc = execSuffixAppended(options.qtInstallDirectory + u'/' + defaultLibexecDir() + - "/rcc"_L1); + rcc = execSuffixAppended(options.qtLibExecsDirectory + "/rcc"_L1); } if (!QFile::exists(rcc)) { @@ -2265,6 +2432,14 @@ bool readDependencies(Options *options) if (!readDependenciesFromElf(options, "%1/libs/%2/lib%3_%2.so"_L1.arg(options->outputDirectory, options->currentArchitecture, options->applicationBinary), &usedDependencies, &remainingDependencies)) return false; + QList<QtDependency> pluginDeps; + for (const auto &pluginPath : options->androidDeployPlugins) { + pluginDeps.append(findFilesRecursively(*options, QFileInfo(pluginPath), + options->qtInstallDirectory + "/"_L1)); + } + + readDependenciesFromFiles(options, pluginDeps, usedDependencies, remainingDependencies); + while (!remainingDependencies.isEmpty()) { QSet<QString>::iterator start = remainingDependencies.begin(); QString fileName = absoluteFilePath(options, *start); @@ -2295,11 +2470,10 @@ bool readDependencies(Options *options) } } - if ((!options->rootPaths.empty() || options->qrcFiles.isEmpty()) && - !scanImports(options, &usedDependencies)) - return false; - - return true; + if (options->qmlSkipImportScanning + || (options->rootPaths.empty() && options->qrcFiles.isEmpty())) + return true; + return scanImports(options, &usedDependencies); } bool containsApplicationBinary(Options *options) @@ -2310,7 +2484,6 @@ bool containsApplicationBinary(Options *options) if (options->verbose) fprintf(stdout, "Checking if application binary is in package.\n"); - QFileInfo applicationBinary(options->applicationBinary); QString applicationFileName = "lib%1_%2.so"_L1.arg(options->applicationBinary, options->currentArchitecture); @@ -2399,17 +2572,13 @@ bool copyQtFiles(Options *options) // Copy other Qt dependencies auto assetsDestinationDirectory = "assets/android_rcc_bundle/"_L1; - for (const QtDependency &qtDependency : qAsConst(options->qtDependencies[options->currentArchitecture])) { + for (const QtDependency &qtDependency : std::as_const(options->qtDependencies[options->currentArchitecture])) { QString sourceFileName = qtDependency.absolutePath; QString destinationFileName; bool isSharedLibrary = qtDependency.relativePath.endsWith(".so"_L1); if (isSharedLibrary) { - QString garbledFileName; - if (QDir::fromNativeSeparators(qtDependency.relativePath).startsWith("lib/"_L1)) { - garbledFileName = qtDependency.relativePath.mid(sizeof("lib/") - 1); - } else { - garbledFileName = qtDependency.relativePath.mid(qtDependency.relativePath.lastIndexOf(u'/') + 1); - } + QString garbledFileName = qtDependency.relativePath.mid( + qtDependency.relativePath.lastIndexOf(u'/') + 1); destinationFileName = libsDirectory + options->currentArchitecture + u'/' + garbledFileName; } else if (QDir::fromNativeSeparators(qtDependency.relativePath).startsWith("jar/"_L1)) { destinationFileName = libsDirectory + qtDependency.relativePath.mid(sizeof("jar/") - 1); @@ -2443,7 +2612,7 @@ bool copyQtFiles(Options *options) *options)) { return false; } - options->bundledFiles[options->currentArchitecture] += qMakePair(destinationFileName, qtDependency.relativePath); + options->bundledFiles[options->currentArchitecture] += std::make_pair(destinationFileName, qtDependency.relativePath); } return true; @@ -2542,7 +2711,7 @@ static bool mergeGradleProperties(const QString &path, GradleProperties properti continue; } } - file.write(line); + file.write(line.trimmed() + '\n'); } oldFile.close(); QFile::remove(oldPathStr); @@ -2559,11 +2728,11 @@ static bool mergeGradleProperties(const QString &path, GradleProperties properti void checkAndWarnGradleLongPaths(const QString &outputDirectory) { QStringList longFileNames; - QDirIterator it(outputDirectory, QStringList(QStringLiteral("*.java")), QDir::Files, - QDirIterator::Subdirectories); - while (it.hasNext()) { - if (it.next().size() >= MAX_PATH) - longFileNames.append(it.next()); + using F = QDirListing::IteratorFlag; + for (const auto &dirEntry : QDirListing(outputDirectory, QStringList(u"*.java"_s), + QDir::Files, F::Recursive)) { + if (dirEntry.size() >= MAX_PATH) + longFileNames.append(dirEntry.filePath()); } if (!longFileNames.isEmpty()) { @@ -2576,6 +2745,38 @@ void checkAndWarnGradleLongPaths(const QString &outputDirectory) } #endif +struct GradleFlags { + bool setsLegacyPackaging = false; + bool usesIntegerCompileSdkVersion = false; +}; + +GradleFlags gradleBuildFlags(const QString &path) +{ + GradleFlags flags; + + QFile file(path); + if (!file.open(QIODevice::ReadOnly)) + return flags; + + auto isComment = [](const QByteArray &line) { + const auto trimmed = line.trimmed(); + return trimmed.startsWith("//") || trimmed.startsWith('*') || trimmed.startsWith("/*"); + }; + + const auto lines = file.readAll().split('\n'); + for (const auto &line : lines) { + if (isComment(line)) + continue; + if (line.contains("useLegacyPackaging")) { + flags.setsLegacyPackaging = true; + } else if (line.contains("compileSdkVersion androidCompileSdkVersion.toInteger()")) { + flags.usesIntegerCompileSdkVersion = true; + } + } + + return flags; +} + bool buildAndroidProject(const Options &options) { GradleProperties localProperties; @@ -2584,16 +2785,46 @@ bool buildAndroidProject(const Options &options) if (!mergeGradleProperties(localPropertiesPath, localProperties)) return false; - QString gradlePropertiesPath = options.outputDirectory + "gradle.properties"_L1; + const QString gradlePropertiesPath = options.outputDirectory + "gradle.properties"_L1; GradleProperties gradleProperties = readGradleProperties(gradlePropertiesPath); - gradleProperties["android.bundle.enableUncompressedNativeLibs"] = "false"; + + const QString gradleBuildFilePath = options.outputDirectory + "build.gradle"_L1; + GradleFlags gradleFlags = gradleBuildFlags(gradleBuildFilePath); + if (!gradleFlags.setsLegacyPackaging) + gradleProperties["android.bundle.enableUncompressedNativeLibs"] = "false"; + gradleProperties["buildDir"] = "build"; - gradleProperties["qtAndroidDir"] = (options.qtInstallDirectory + "/src/android/java"_L1).toUtf8(); + gradleProperties["qtAndroidDir"] = + (options.qtInstallDirectory + u'/' + options.qtDataDirectory + + "/src/android/java"_L1) + .toUtf8(); // The following property "qt5AndroidDir" is only for compatibility. // Projects using a custom build.gradle file may use this variable. // ### Qt7: Remove the following line - gradleProperties["qt5AndroidDir"] = (options.qtInstallDirectory + "/src/android/java"_L1).toUtf8(); - gradleProperties["androidCompileSdkVersion"] = options.androidPlatform.split(u'-').last().toLocal8Bit(); + gradleProperties["qt5AndroidDir"] = + (options.qtInstallDirectory + u'/' + options.qtDataDirectory + + "/src/android/java"_L1) + .toUtf8(); + + QByteArray sdkPlatformVersion; + // Provide the integer version only if build.gradle explicitly converts to Integer, + // to avoid regression to existing projects that build for sdk platform of form android-xx. + if (gradleFlags.usesIntegerCompileSdkVersion) { + const QByteArray tmp = options.androidPlatform.split(u'-').last().toLocal8Bit(); + bool ok; + tmp.toInt(&ok); + if (ok) { + sdkPlatformVersion = tmp; + } else { + fprintf(stderr, "Warning: Gradle expects SDK platform version to be an integer, " + "but the set version is not convertible to an integer."); + } + } + + if (sdkPlatformVersion.isEmpty()) + sdkPlatformVersion = options.androidPlatform.toLocal8Bit(); + + gradleProperties["androidCompileSdkVersion"] = sdkPlatformVersion; gradleProperties["qtMinSdkVersion"] = options.minSdkVersion; gradleProperties["qtTargetSdkVersion"] = options.targetSdkVersion; gradleProperties["androidNdkVersion"] = options.ndkVersion.toUtf8(); @@ -2811,7 +3042,7 @@ static QString zipalignPath(const Options &options, bool *ok) return zipAlignTool; } -bool jarSignerSignPackage(const Options &options) +bool signAAB(const Options &options) { if (options.verbose) fprintf(stdout, "Signing Android package.\n"); @@ -2865,7 +3096,7 @@ bool jarSignerSignPackage(const Options &options) if (options.protectedAuthenticationPath) jarSignerTool += " -protected"_L1; - auto signPackage = [&](const QString &file) { + auto jarSignPackage = [&](const QString &file) { fprintf(stdout, "Signing file %s\n", qPrintable(file)); fflush(stdout); QString command = jarSignerTool + " %1 %2"_L1.arg(shellQuote(file)) @@ -2893,49 +3124,15 @@ bool jarSignerSignPackage(const Options &options) return true; }; - if (!signPackage(packagePath(options, UnsignedAPK))) - return false; - if (options.buildAAB && !signPackage(packagePath(options, AAB))) - return false; - - bool ok; - QString zipAlignTool = zipalignPath(options, &ok); - if (!ok) + if (options.buildAAB && !jarSignPackage(packagePath(options, AAB))) return false; - - zipAlignTool = "%1%2 -f 4 %3 %4"_L1.arg(shellQuote(zipAlignTool), - options.verbose ? " -v"_L1 : QLatin1StringView(), - shellQuote(packagePath(options, UnsignedAPK)), - shellQuote(packagePath(options, SignedAPK))); - - FILE *zipAlignCommand = openProcess(zipAlignTool); - if (zipAlignCommand == 0) { - fprintf(stderr, "Couldn't run zipalign.\n"); - return false; - } - - char buffer[512]; - while (fgets(buffer, sizeof(buffer), zipAlignCommand) != 0) - fprintf(stdout, "%s", buffer); - - int errorCode = pclose(zipAlignCommand); - if (errorCode != 0) { - fprintf(stderr, "zipalign command failed.\n"); - if (!options.verbose) - fprintf(stderr, " -- Run with --verbose for more information.\n"); - return false; - } - - return QFile::remove(packagePath(options, UnsignedAPK)); + return true; } bool signPackage(const Options &options) { const QString apksignerTool = batSuffixAppended(options.sdkPath + "/build-tools/"_L1 + options.sdkBuildToolsVersion + "/apksigner"_L1); - if (options.jarSigner || !QFile::exists(apksignerTool)) - return jarSignerSignPackage(options); - // APKs signed with apksigner must not be changed after they're signed, // therefore we need to zipalign it before we sign it. @@ -3040,6 +3237,9 @@ bool signPackage(const Options &options) "%1 verify --verbose %2"_L1 .arg(shellQuote(apksignerTool), shellQuote(packagePath(options, SignedAPK))); + if (options.buildAAB && !signAAB(options)) + return false; + // Verify the package and remove the unsigned apk return apkSignerRunner(apkVerifyCommand, true) && QFile::remove(packagePath(options, UnsignedAPK)); } @@ -3135,10 +3335,12 @@ int main(int argc, char *argv[]) for (auto it = options.architectures.constBegin(); it != options.architectures.constEnd(); ++it) { if (!it->enabled) continue; - options.setCurrentQtArchitecture(it.key(), it.value().qtInstallDirectory); + options.setCurrentQtArchitecture(it.key(), + it.value().qtInstallDirectory, + it.value().qtDirectories); // All architectures have a copy of the gradle files but only one set needs to be copied. - if (!androidTemplatetCopied && options.build && !options.auxMode && !options.copyDependenciesOnly) { + if (!androidTemplatetCopied && options.build && !options.copyDependenciesOnly) { cleanAndroidFiles(options); if (Q_UNLIKELY(options.timing)) fprintf(stdout, "[TIMING] %lld ns: Cleaned Android file\n", options.timer.nsecsElapsed()); @@ -3175,13 +3377,12 @@ int main(int argc, char *argv[]) if (Q_UNLIKELY(options.timing)) fprintf(stdout, "[TIMING] %lld ns: Copied extra resources\n", options.timer.nsecsElapsed()); - if (!options.auxMode) { - if (!copyStdCpp(&options)) - return CannotCopyGnuStl; + if (!copyStdCpp(&options)) + return CannotCopyGnuStl; + + if (Q_UNLIKELY(options.timing)) + fprintf(stdout, "[TIMING] %lld ns: Copied GNU STL\n", options.timer.nsecsElapsed()); - if (Q_UNLIKELY(options.timing)) - fprintf(stdout, "[TIMING] %lld ns: Copied GNU STL\n", options.timer.nsecsElapsed()); - } // If Unbundled deployment is used, remove app lib as we don't want it packaged inside the APK if (options.deploymentMechanism == Options::Unbundled) { QString appLibPath = "%1/libs/%2/lib%3_%2.so"_L1. |