summaryrefslogtreecommitdiffstats
path: root/src/tools/androiddeployqt
diff options
context:
space:
mode:
Diffstat (limited to 'src/tools/androiddeployqt')
-rw-r--r--src/tools/androiddeployqt/CMakeLists.txt13
-rw-r--r--src/tools/androiddeployqt/doc/src/androiddeployqt.qdoc231
-rw-r--r--src/tools/androiddeployqt/main.cpp1019
3 files changed, 697 insertions, 566 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 dbb4bbd24e..b5751e9a40 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>
@@ -44,15 +44,16 @@ static const bool mustReadOutputAnyway = true; // pclose seems to return the wro
static QStringList dependenciesForDepfile;
-FILE *openProcess(const QString &command)
+auto openProcess(const QString &command)
{
#if defined(Q_OS_WIN32)
QString processedCommand = u'\"' + command + u'\"';
#else
const QString& processedCommand = command;
#endif
-
- return popen(processedCommand.toLocal8Bit().constData(), QT_POPEN_READ);
+ struct Closer { void operator()(FILE *proc) const { if (proc) (void)pclose(proc); } };
+ using UP = std::unique_ptr<FILE, Closer>;
+ return UP{popen(processedCommand.toLocal8Bit().constData(), QT_POPEN_READ)};
}
struct QtDependency
@@ -70,10 +71,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,10 +102,10 @@ struct Options
, internalSf(false)
, sectionsOnly(false)
, protectedAuthenticationPath(false)
- , jarSigner(false)
, installApk(false)
, uninstallApk(false)
, qmlImportScannerBinaryPath()
+ , buildAar(false)
{}
enum DeploymentMechanism
@@ -129,8 +138,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;
@@ -149,8 +165,8 @@ struct Options
// Versioning
QString versionName;
QString versionCode;
- QByteArray minSdkVersion{"23"};
- QByteArray targetSdkVersion{"30"};
+ QByteArray minSdkVersion{"28"};
+ QByteArray targetSdkVersion{"34"};
// lib c++ path
QString stdCppPath;
@@ -191,7 +207,6 @@ struct Options
bool internalSf;
bool sectionsOnly;
bool protectedAuthenticationPath;
- bool jarSigner;
QString apkPath;
// Installation information
@@ -200,12 +215,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 +240,8 @@ struct Options
// Override qml import scanner path
QString qmlImportScannerBinaryPath;
+ bool qmlSkipImportScanning = false;
+ bool buildAar;
};
static const QHash<QByteArray, QByteArray> elfArchitectures = {
@@ -227,6 +251,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$"));
@@ -281,26 +311,23 @@ QString fileArchitecture(const Options &options, const QString &path)
return {};
}
- readElf = "%1 -needed-libs %2"_L1.arg(shellQuote(readElf), shellQuote(path));
+ readElf = "%1 --needed-libs %2"_L1.arg(shellQuote(readElf), shellQuote(path));
- FILE *readElfCommand = openProcess(readElf);
+ auto readElfCommand = openProcess(readElf);
if (!readElfCommand) {
fprintf(stderr, "Cannot execute command %s\n", qPrintable(readElf));
return {};
}
char buffer[512];
- while (fgets(buffer, sizeof(buffer), readElfCommand) != nullptr) {
+ while (fgets(buffer, sizeof(buffer), readElfCommand.get()) != nullptr) {
QByteArray line = QByteArray::fromRawData(buffer, qstrlen(buffer));
- QString library;
line = line.trimmed();
if (line.startsWith("Arch: ")) {
auto it = elfArchitectures.find(line.mid(6));
- pclose(readElfCommand);
return it != elfArchitectures.constEnd() ? QString::fromLatin1(it.value()) : QString{};
}
}
- pclose(readElfCommand);
return {};
}
@@ -321,7 +348,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 +386,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,18 +451,22 @@ 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 "
"environment variables.\n");
- options.releasePackage = true;
options.keyStore = keyStore;
options.keyStoreAlias = storeAlias;
}
- } else {
- options.releasePackage = true;
+ } 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
@@ -496,10 +526,10 @@ 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("--build-aar"_L1, Qt::CaseInsensitive) == 0) {
+ options.buildAar = true;
} else if (argument.compare("--qml-importscanner-binary"_L1, Qt::CaseInsensitive) == 0) {
options.qmlImportScannerBinaryPath = arguments.at(++i).trimmed();
} else if (argument.compare("--no-rcc-bundle-cleanup"_L1,
@@ -511,6 +541,23 @@ Options parseOptions()
}
}
+ if (options.buildAar) {
+ if (options.installApk || options.uninstallApk) {
+ fprintf(stderr, "Warning: Skipping %s, AAR packages are not installable.\n",
+ options.uninstallApk ? "--reinstall" : "--install");
+ options.installApk = false;
+ options.uninstallApk = false;
+ }
+ if (options.buildAAB) {
+ fprintf(stderr, "Warning: Skipping -aab as --build-aar is present.\n");
+ options.buildAAB = false;
+ }
+ if (!options.keyStore.isEmpty()) {
+ fprintf(stderr, "Warning: Skipping --sign, signing AAR packages is not supported.\n");
+ options.keyStore.clear();
+ }
+ }
+
if (options.buildDirectory.isEmpty() && !options.depFilePath.isEmpty())
options.helpRequested = true;
@@ -533,114 +580,115 @@ 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. Also implies the\n"
- " --release option.\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.
+
+ --build-aar: Build an AAR package. This option skips --aab, --install,
+ --reinstall, and --sign options if they are provided.
+
+ --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,
@@ -651,10 +699,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
@@ -707,18 +755,72 @@ bool copyFileIfNewer(const QString &sourceFileName,
return true;
}
-QString cleanPackageName(QString packageName)
+struct GradleBuildConfigs {
+ QString appNamespace;
+ bool setsLegacyPackaging = false;
+ bool usesIntegerCompileSdkVersion = false;
+};
+
+GradleBuildConfigs gradleBuildConfigs(const QString &path)
+{
+ GradleBuildConfigs configs;
+
+ QFile file(path);
+ if (!file.open(QIODevice::ReadOnly))
+ return configs;
+
+ auto isComment = [](const QByteArray &trimmed) {
+ return trimmed.startsWith("//") || trimmed.startsWith('*') || trimmed.startsWith("/*");
+ };
+
+ auto extractValue = [](const QByteArray &trimmed) {
+ int idx = trimmed.indexOf('=');
+
+ if (idx == -1)
+ idx = trimmed.indexOf(' ');
+
+ if (idx > -1)
+ return trimmed.mid(idx + 1).trimmed();
+
+ return QByteArray();
+ };
+
+ const auto lines = file.readAll().split('\n');
+ for (const auto &line : lines) {
+ const QByteArray trimmedLine = line.trimmed();
+ if (isComment(trimmedLine))
+ continue;
+ if (trimmedLine.contains("useLegacyPackaging")) {
+ configs.setsLegacyPackaging = true;
+ } else if (trimmedLine.contains("compileSdkVersion androidCompileSdkVersion.toInteger()")) {
+ configs.usesIntegerCompileSdkVersion = true;
+ } else if (trimmedLine.contains("namespace")) {
+ configs.appNamespace = QString::fromUtf8(extractValue(trimmedLine));
+ }
+ }
+
+ return configs;
+}
+
+QString cleanPackageName(QString packageName, bool *cleaned = nullptr)
{
auto isLegalChar = [] (QChar c) -> bool {
ushort ch = c.unicode();
return (ch >= '0' && ch <= '9') ||
(ch >= 'A' && ch <= 'Z') ||
(ch >= 'a' && ch <= 'z') ||
- ch == '.';
+ ch == '.' || ch == '_';
};
+
+ if (cleaned)
+ *cleaned = false;
+
for (QChar &c : packageName) {
- if (!isLegalChar(c))
+ if (!isLegalChar(c)) {
c = u'_';
+ if (cleaned)
+ *cleaned = true;
+ }
}
static QStringList keywords;
@@ -744,21 +846,25 @@ 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];
if ((c >= u'0' && c <= u'9') || c == u'_') {
packageName.insert(index + 1, u'a');
+ if (cleaned)
+ *cleaned = true;
index = next + 1;
continue;
}
}
if (keywords.contains(word)) {
packageName.insert(next, "_"_L1);
+ if (cleaned)
+ *cleaned = true;
index = next + 1;
} else {
index = next;
@@ -784,22 +890,35 @@ QString detectLatestAndroidPlatform(const QString &sdkPath)
std::sort(fileInfos.begin(), fileInfos.end(), quasiLexicographicalReverseLessThan);
- QFileInfo latestPlatform = fileInfos.first();
+ const QFileInfo& latestPlatform = fileInfos.constFirst();
return latestPlatform.baseName();
}
-QString packageNameFromAndroidManifest(const QString &androidManifestPath)
+QString extractPackageName(Options *options)
{
- QFile androidManifestXml(androidManifestPath);
+ {
+ const QString gradleBuildFile = options->androidSourceDirectory + "/build.gradle"_L1;
+ QString packageName = gradleBuildConfigs(gradleBuildFile).appNamespace;
+
+ if (!packageName.isEmpty() && packageName != "androidPackageName"_L1)
+ return packageName;
+ }
+
+ QFile androidManifestXml(options->androidSourceDirectory + "/AndroidManifest.xml"_L1);
if (androidManifestXml.open(QIODevice::ReadOnly)) {
QXmlStreamReader reader(&androidManifestXml);
while (!reader.atEnd()) {
reader.readNext();
- if (reader.isStartElement() && reader.name() == "manifest"_L1)
- return cleanPackageName(reader.attributes().value("package"_L1).toString());
+ if (reader.isStartElement() && reader.name() == "manifest"_L1) {
+ QString packageName = reader.attributes().value("package"_L1).toString();
+ if (!packageName.isEmpty() && packageName != "org.qtproject.example"_L1)
+ return packageName;
+ break;
+ }
}
}
- return {};
+
+ return QString();
}
bool parseCmakeBoolean(const QJsonValue &value)
@@ -812,6 +931,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);
@@ -868,7 +1047,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;
}
@@ -896,6 +1076,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()) {
@@ -918,6 +1106,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) {
@@ -1033,6 +1226,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',');
@@ -1074,7 +1273,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());
}
}
@@ -1117,6 +1316,25 @@ bool readInputFile(Options *options)
}
{
+ const QJsonValue androidPackageName = jsonObject.value("android-package-name"_L1);
+ const QString extractedPackageName = extractPackageName(options);
+ if (!extractedPackageName.isEmpty())
+ options->packageName = extractedPackageName;
+ else if (!androidPackageName.isUndefined())
+ options->packageName = androidPackageName.toString();
+ else
+ options->packageName = "org.qtproject.example.%1"_L1.arg(options->applicationBinary);
+
+ bool cleaned;
+ options->packageName = cleanPackageName(options->packageName, &cleaned);
+ if (cleaned) {
+ fprintf(stderr, "Warning: Package name contained illegal characters and was cleaned "
+ "to \"%s\"\n", qPrintable(options->packageName));
+ }
+ }
+
+ {
+ using ItFlag = QDirListing::IteratorFlag;
const QJsonValue deploymentDependencies = jsonObject.value("deployment-dependencies"_L1);
if (!deploymentDependencies.isUndefined()) {
QString deploymentDependenciesString = deploymentDependencies.toString();
@@ -1125,14 +1343,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));
@@ -1141,12 +1357,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));
}
}
}
@@ -1162,9 +1390,6 @@ bool readInputFile(Options *options)
options->isZstdCompressionEnabled = zstdCompressionFlag.toBool();
}
}
- options->packageName = packageNameFromAndroidManifest(options->androidSourceDirectory + "/AndroidManifest.xml"_L1);
- if (options->packageName.isEmpty())
- options->packageName = cleanPackageName("org.qtproject.example.%1"_L1.arg(options->applicationBinary));
return true;
}
@@ -1202,7 +1427,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()));
}
}
@@ -1211,13 +1436,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;
@@ -1235,7 +1462,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;
@@ -1261,6 +1489,9 @@ bool copyAndroidTemplate(const Options &options)
if (!copyAndroidTemplate(options, "/src/android/templates"_L1))
return false;
+ if (options.buildAar)
+ return copyAndroidTemplate(options, "/src/android/templates_aar"_L1);
+
return true;
}
@@ -1360,7 +1591,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);
@@ -1406,7 +1638,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;
@@ -1447,7 +1679,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()]) {
@@ -1489,27 +1720,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;
}
@@ -1592,12 +1817,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;
@@ -1622,16 +1847,10 @@ bool updateAndroidManifest(Options &options)
reader.readNext();
if (reader.isStartElement()) {
- if (reader.name() == "manifest"_L1) {
- if (!reader.attributes().hasAttribute("package"_L1)) {
- fprintf(stderr, "Invalid android manifest file: %s\n", qPrintable(androidManifestPath));
- return false;
- }
- options.packageName = reader.attributes().value("package"_L1).toString();
- } else if (reader.name() == "uses-sdk"_L1) {
+ if (reader.name() == "uses-sdk"_L1) {
if (reader.attributes().hasAttribute("android:minSdkVersion"_L1))
- if (reader.attributes().value("android:minSdkVersion"_L1).toInt() < 23) {
- fprintf(stderr, "Invalid minSdkVersion version, minSdkVersion must be >= 23\n");
+ if (reader.attributes().value("android:minSdkVersion"_L1).toInt() < 28) {
+ fprintf(stderr, "Invalid minSdkVersion version, minSdkVersion must be >= 28\n");
return false;
}
} else if ((reader.name() == "application"_L1 ||
@@ -1698,6 +1917,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;
}
@@ -1720,19 +1959,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,
@@ -1740,7 +2023,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()) {
@@ -1765,23 +2048,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());
@@ -1843,9 +2118,9 @@ QStringList getQtLibsFromElf(const Options &options, const QString &fileName)
return QStringList();
}
- readElf = "%1 -needed-libs %2"_L1.arg(shellQuote(readElf), shellQuote(fileName));
+ readElf = "%1 --needed-libs %2"_L1.arg(shellQuote(readElf), shellQuote(fileName));
- FILE *readElfCommand = openProcess(readElf);
+ auto readElfCommand = openProcess(readElf);
if (!readElfCommand) {
fprintf(stderr, "Cannot execute command %s\n", qPrintable(readElf));
return QStringList();
@@ -1855,7 +2130,7 @@ QStringList getQtLibsFromElf(const Options &options, const QString &fileName)
bool readLibs = false;
char buffer[512];
- while (fgets(buffer, sizeof(buffer), readElfCommand) != nullptr) {
+ while (fgets(buffer, sizeof(buffer), readElfCommand.get()) != nullptr) {
QByteArray line = QByteArray::fromRawData(buffer, qstrlen(buffer));
QString library;
line = line.trimmed();
@@ -1879,8 +2154,6 @@ QStringList getQtLibsFromElf(const Options &options, const QString &fileName)
ret += libraryName;
}
- pclose(readElfCommand);
-
return ret;
}
@@ -1918,7 +2191,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)) {
@@ -1929,9 +2202,6 @@ bool readDependenciesFromElf(Options *options,
return true;
}
-bool goodToCopy(const Options *options, const QString &file, QStringList *unmetDependencies);
-bool checkQmlFileInRootPaths(const Options *options, const QString &absolutePath);
-
bool scanImports(Options *options, QSet<QString> *usedDependencies)
{
if (options->verbose)
@@ -1941,19 +2211,29 @@ 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;
- importPaths += shellQuote(options->qtInstallDirectory + "/qml"_L1);
+ // In Conan's case, qtInstallDirectory will point only to qtbase installed files, which
+ // 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 + u'/' + options->qtQmlDirectory;
+ if (QFile::exists(mainImportPath))
+ importPaths += shellQuote(mainImportPath);
+
+ // These are usually provided by CMake in the deployment json file from paths specified
+ // in CMAKE_FIND_ROOT_PATH. They might not have qml modules.
for (const QString &prefix : options->extraPrefixDirs)
- if (QDir().exists(prefix + "/qml"_L1))
+ if (QFile::exists(prefix + "/qml"_L1))
importPaths += shellQuote(prefix + "/qml"_L1);
- for (const QString &qmlImportPath : qAsConst(options->qmlImportPaths)) {
- if (QDir().exists(qmlImportPath)) {
+ // These are provided by both CMake and qmake.
+ for (const QString &qmlImportPath : std::as_const(options->qmlImportPaths)) {
+ if (QFile::exists(qmlImportPath)) {
importPaths += shellQuote(qmlImportPath);
} else {
fprintf(stderr, "Warning: QML import path %s does not exist.\n",
@@ -2011,7 +2291,7 @@ bool scanImports(Options *options, QSet<QString> *usedDependencies)
qmlImportScanner.toLocal8Bit().constData());
}
- FILE *qmlImportScannerCommand = popen(qmlImportScanner.toLocal8Bit().constData(), QT_POPEN_READ);
+ auto qmlImportScannerCommand = openProcess(qmlImportScanner);
if (qmlImportScannerCommand == 0) {
fprintf(stderr, "Couldn't run qmlimportscanner.\n");
return false;
@@ -2019,7 +2299,7 @@ bool scanImports(Options *options, QSet<QString> *usedDependencies)
QByteArray output;
char buffer[512];
- while (fgets(buffer, sizeof(buffer), qmlImportScannerCommand) != 0)
+ while (fgets(buffer, sizeof(buffer), qmlImportScannerCommand.get()) != nullptr)
output += QByteArray(buffer, qstrlen(buffer));
QJsonDocument jsonDocument = QJsonDocument::fromJson(output);
@@ -2058,21 +2338,19 @@ bool scanImports(Options *options, QSet<QString> *usedDependencies)
if (!absolutePath.endsWith(u'/'))
absolutePath += u'/';
- if (checkQmlFileInRootPaths(options, absolutePath)) {
+ const QUrl url(object.value("name"_L1).toString());
+
+ 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;
}
@@ -2150,10 +2428,11 @@ bool scanImports(Options *options, QSet<QString> *usedDependencies)
return true;
}
-bool checkQmlFileInRootPaths(const Options *options, const QString &absolutePath)
+bool checkCanImportFromRootPaths(const Options *options, const QString &absolutePath,
+ const QString &moduleUrlPath)
{
for (auto rootPath : options->rootPaths) {
- if (absolutePath.startsWith(rootPath))
+ if ((rootPath + moduleUrlPath) == absolutePath)
return true;
}
return false;
@@ -2164,17 +2443,17 @@ bool runCommand(const Options &options, const QString &command)
if (options.verbose)
fprintf(stdout, "Running command '%s'\n", qPrintable(command));
- FILE *runCommand = openProcess(command);
+ auto runCommand = openProcess(command);
if (runCommand == nullptr) {
fprintf(stderr, "Cannot run command '%s'\n", qPrintable(command));
return false;
}
char buffer[4096];
- while (fgets(buffer, sizeof(buffer), runCommand) != nullptr) {
+ while (fgets(buffer, sizeof(buffer), runCommand.get()) != nullptr) {
if (options.verbose)
fprintf(stdout, "%s", buffer);
}
- pclose(runCommand);
+ runCommand.reset();
fflush(stdout);
fflush(stderr);
return true;
@@ -2196,8 +2475,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)) {
@@ -2253,6 +2531,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);
@@ -2283,11 +2569,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)
@@ -2298,7 +2583,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);
@@ -2320,7 +2604,8 @@ bool containsApplicationBinary(Options *options)
return true;
}
-FILE *runAdb(const Options &options, const QString &arguments)
+auto runAdb(const Options &options, const QString &arguments)
+ -> decltype(openProcess({}))
{
QString adb = execSuffixAppended(options.sdkPath + "/platform-tools/adb"_L1);
if (!QFile::exists(adb)) {
@@ -2336,7 +2621,7 @@ FILE *runAdb(const Options &options, const QString &arguments)
if (options.verbose)
fprintf(stdout, "Running command \"%s\"\n", adb.toLocal8Bit().constData());
- FILE *adbCommand = openProcess(adb);
+ auto adbCommand = openProcess(adb);
if (adbCommand == 0) {
fprintf(stderr, "Cannot start adb: %s\n", qPrintable(adb));
return 0;
@@ -2387,17 +2672,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);
@@ -2431,7 +2712,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;
@@ -2530,7 +2811,7 @@ static bool mergeGradleProperties(const QString &path, GradleProperties properti
continue;
}
}
- file.write(line);
+ file.write(line.trimmed() + '\n');
}
oldFile.close();
QFile::remove(oldPathStr);
@@ -2547,11 +2828,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()) {
@@ -2572,16 +2853,47 @@ 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;
+ GradleBuildConfigs gradleConfigs = gradleBuildConfigs(gradleBuildFilePath);
+ if (!gradleConfigs.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 (gradleConfigs.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["androidPackageName"] = options.packageName.toLocal8Bit();
+ gradleProperties["androidCompileSdkVersion"] = sdkPlatformVersion;
gradleProperties["qtMinSdkVersion"] = options.minSdkVersion;
gradleProperties["qtTargetSdkVersion"] = options.targetSdkVersion;
gradleProperties["androidNdkVersion"] = options.ndkVersion.toUtf8();
@@ -2596,6 +2908,9 @@ bool buildAndroidProject(const Options &options)
abiList.append(it.key());
}
gradleProperties["qtTargetAbiList"] = abiList.toLocal8Bit();// armeabi-v7a or arm64-v8a or ...
+ gradleProperties["qtGradlePluginType"] = options.buildAar
+ ? "com.android.library"
+ : "com.android.application";
if (!mergeGradleProperties(gradlePropertiesPath, gradleProperties))
return false;
@@ -2621,19 +2936,19 @@ bool buildAndroidProject(const Options &options)
if (options.verbose)
commandLine += " --info"_L1;
- FILE *gradleCommand = openProcess(commandLine);
+ auto gradleCommand = openProcess(commandLine);
if (gradleCommand == 0) {
fprintf(stderr, "Cannot run gradle command: %s\n.", qPrintable(commandLine));
return false;
}
char buffer[512];
- while (fgets(buffer, sizeof(buffer), gradleCommand) != 0) {
+ while (fgets(buffer, sizeof(buffer), gradleCommand.get()) != nullptr) {
fprintf(stdout, "%s", buffer);
fflush(stdout);
}
- int errorCode = pclose(gradleCommand);
+ const int errorCode = pclose(gradleCommand.release());
if (errorCode != 0) {
fprintf(stderr, "Building the android package failed!\n");
if (!options.verbose)
@@ -2659,18 +2974,18 @@ bool uninstallApk(const Options &options)
fprintf(stdout, "Uninstalling old Android package %s if present.\n", qPrintable(options.packageName));
- FILE *adbCommand = runAdb(options, " uninstall "_L1 + shellQuote(options.packageName));
+ auto adbCommand = runAdb(options, " uninstall "_L1 + shellQuote(options.packageName));
if (adbCommand == 0)
return false;
if (options.verbose || mustReadOutputAnyway) {
char buffer[512];
- while (fgets(buffer, sizeof(buffer), adbCommand) != 0)
+ while (fgets(buffer, sizeof(buffer), adbCommand.get()) != nullptr)
if (options.verbose)
fprintf(stdout, "%s", buffer);
}
- int returnCode = pclose(adbCommand);
+ const int returnCode = pclose(adbCommand.release());
if (returnCode != 0) {
fprintf(stderr, "Warning: Uninstall failed!\n");
if (!options.verbose)
@@ -2683,39 +2998,43 @@ bool uninstallApk(const Options &options)
enum PackageType {
AAB,
+ AAR,
UnsignedAPK,
SignedAPK
};
-QString packagePath(const Options &options, PackageType pt)
-{
- QString path(options.outputDirectory);
- path += "/build/outputs/%1/"_L1.arg(pt >= UnsignedAPK ? QStringLiteral("apk") : QStringLiteral("bundle"));
- QString buildType(options.releasePackage ? "release/"_L1 : "debug/"_L1);
- if (QDir(path + buildType).exists())
- path += buildType;
- path += QDir(options.outputDirectory).dirName() + u'-';
- if (options.releasePackage) {
- path += "release-"_L1;
- if (pt >= UnsignedAPK) {
- if (pt == UnsignedAPK)
- path += "un"_L1;
- path += "signed.apk"_L1;
- } else {
- path.chop(1);
- path += ".aab"_L1;
- }
- } else {
- path += "debug"_L1;
- if (pt >= UnsignedAPK) {
- if (pt == SignedAPK)
- path += "-signed"_L1;
- path += ".apk"_L1;
- } else {
- path += ".aab"_L1;
- }
- }
- return path;
+QString packagePath(const Options &options, PackageType packageType)
+{
+ // The package type is always AAR if option.buildAar has been set
+ if (options.buildAar)
+ packageType = AAR;
+
+ static const QHash<PackageType, QLatin1StringView> packageTypeToPath{
+ { AAB, "bundle"_L1 }, { AAR, "aar"_L1 }, { UnsignedAPK, "apk"_L1 }, { SignedAPK, "apk"_L1 }
+ };
+ static const QHash<PackageType, QLatin1StringView> packageTypeToExtension{
+ { AAB, "aab"_L1 }, { AAR, "aar"_L1 }, { UnsignedAPK, "apk"_L1 }, { SignedAPK, "apk"_L1 }
+ };
+
+ const QString buildType(options.releasePackage ? "release"_L1 : "debug"_L1);
+ QString signedSuffix;
+ if (packageType == SignedAPK)
+ signedSuffix = "-signed"_L1;
+ else if (packageType == UnsignedAPK && options.releasePackage)
+ signedSuffix = "-unsigned"_L1;
+
+ QString dirPath(options.outputDirectory);
+ dirPath += "/build/outputs/%1/"_L1.arg(packageTypeToPath[packageType]);
+ if (QDir(dirPath + buildType).exists())
+ dirPath += buildType;
+
+ const QString fileName = "/%1-%2%3.%4"_L1.arg(
+ QDir(options.outputDirectory).dirName(),
+ buildType,
+ signedSuffix,
+ packageTypeToExtension[packageType]);
+
+ return dirPath + fileName;
}
bool installApk(const Options &options)
@@ -2728,20 +3047,20 @@ bool installApk(const Options &options)
if (options.verbose)
fprintf(stdout, "Installing Android package to device.\n");
- FILE *adbCommand = runAdb(options, " install -r "_L1
- + packagePath(options, options.keyStore.isEmpty() ? UnsignedAPK
- : SignedAPK));
+ auto adbCommand = runAdb(options, " install -r "_L1
+ + packagePath(options, options.keyStore.isEmpty() ? UnsignedAPK
+ : SignedAPK));
if (adbCommand == 0)
return false;
if (options.verbose || mustReadOutputAnyway) {
char buffer[512];
- while (fgets(buffer, sizeof(buffer), adbCommand) != 0)
+ while (fgets(buffer, sizeof(buffer), adbCommand.get()) != nullptr)
if (options.verbose)
fprintf(stdout, "%s", buffer);
}
- int returnCode = pclose(adbCommand);
+ const int returnCode = pclose(adbCommand.release());
if (returnCode != 0) {
fprintf(stderr, "Installing to device failed!\n");
if (!options.verbose)
@@ -2799,7 +3118,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");
@@ -2853,13 +3172,13 @@ 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))
.arg(shellQuote(options.keyStoreAlias));
- FILE *jarSignerCommand = openProcess(command);
+ auto jarSignerCommand = openProcess(command);
if (jarSignerCommand == 0) {
fprintf(stderr, "Couldn't run jarsigner.\n");
return false;
@@ -2867,11 +3186,11 @@ bool jarSignerSignPackage(const Options &options)
if (options.verbose) {
char buffer[512];
- while (fgets(buffer, sizeof(buffer), jarSignerCommand) != 0)
+ while (fgets(buffer, sizeof(buffer), jarSignerCommand.get()) != nullptr)
fprintf(stdout, "%s", buffer);
}
- int errorCode = pclose(jarSignerCommand);
+ const int errorCode = pclose(jarSignerCommand.release());
if (errorCode != 0) {
fprintf(stderr, "jarsigner command failed.\n");
if (!options.verbose)
@@ -2881,49 +3200,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)
- 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");
+ if (options.buildAAB && !jarSignPackage(packagePath(options, AAB)))
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.
@@ -2933,17 +3218,17 @@ bool signPackage(const Options &options)
return false;
auto zipalignRunner = [](const QString &zipAlignCommandLine) {
- FILE *zipAlignCommand = openProcess(zipAlignCommandLine);
+ auto zipAlignCommand = openProcess(zipAlignCommandLine);
if (zipAlignCommand == 0) {
fprintf(stderr, "Couldn't run zipalign.\n");
return false;
}
char buffer[512];
- while (fgets(buffer, sizeof(buffer), zipAlignCommand) != 0)
+ while (fgets(buffer, sizeof(buffer), zipAlignCommand.get()) != nullptr)
fprintf(stdout, "%s", buffer);
- return pclose(zipAlignCommand) == 0;
+ return pclose(zipAlignCommand.release()) == 0;
};
const QString verifyZipAlignCommandLine =
@@ -3000,17 +3285,17 @@ bool signPackage(const Options &options)
apkSignCommand += " %1"_L1.arg(shellQuote(packagePath(options, SignedAPK)));
auto apkSignerRunner = [](const QString &command, bool verbose) {
- FILE *apkSigner = openProcess(command);
+ auto apkSigner = openProcess(command);
if (apkSigner == 0) {
fprintf(stderr, "Couldn't run apksigner.\n");
return false;
}
char buffer[512];
- while (fgets(buffer, sizeof(buffer), apkSigner) != 0)
+ while (fgets(buffer, sizeof(buffer), apkSigner.get()) != nullptr)
fprintf(stdout, "%s", buffer);
- int errorCode = pclose(apkSigner);
+ const int errorCode = pclose(apkSigner.release());
if (errorCode != 0) {
fprintf(stderr, "apksigner command failed.\n");
if (!verbose)
@@ -3028,6 +3313,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));
}
@@ -3123,10 +3411,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());
@@ -3163,13 +3453,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.