summaryrefslogtreecommitdiffstats
path: root/src/tools/androiddeployqt
diff options
context:
space:
mode:
Diffstat (limited to 'src/tools/androiddeployqt')
-rw-r--r--src/tools/androiddeployqt/CMakeLists.txt8
-rw-r--r--src/tools/androiddeployqt/doc/src/androiddeployqt.qdoc231
-rw-r--r--src/tools/androiddeployqt/main.cpp1138
3 files changed, 894 insertions, 483 deletions
diff --git a/src/tools/androiddeployqt/CMakeLists.txt b/src/tools/androiddeployqt/CMakeLists.txt
index b53e541fd0..041d883877 100644
--- a/src/tools/androiddeployqt/CMakeLists.txt
+++ b/src/tools/androiddeployqt/CMakeLists.txt
@@ -1,8 +1,6 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause
-# Generated from androiddeployqt.pro.
-
#####################################################################
## androiddeployqt App:
#####################################################################
@@ -19,8 +17,9 @@ qt_internal_add_tool(${target_name}
QT_NO_CAST_FROM_ASCII
QT_NO_CAST_TO_ASCII
QT_NO_FOREACH
+ QT_NO_QPAIR
LIBRARIES
- Qt::Core # special case
+ Qt::Core
INCLUDE_DIRECTORIES
../shared
)
@@ -29,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 75a2ddd4d5..eab943c049 100644
--- a/src/tools/androiddeployqt/main.cpp
+++ b/src/tools/androiddeployqt/main.cpp
@@ -13,13 +13,14 @@
#include <QXmlStreamReader>
#include <QStandardPaths>
#include <QUuid>
-#include <QDirIterator>
+#include <QDirListing>
#include <QElapsedTimer>
#include <QRegularExpression>
#include <QSettings>
#include <QHash>
#include <QSet>
#include <QMap>
+#include <QProcess>
#include <depfile_shared.h>
#include <shellquote_shared.h>
@@ -44,15 +45,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
@@ -104,6 +106,9 @@ struct Options
, installApk(false)
, uninstallApk(false)
, qmlImportScannerBinaryPath()
+ , buildAar(false)
+ , qmlDomBinaryPath()
+ , generateJavaQmlComponents(false)
{}
enum DeploymentMechanism
@@ -144,6 +149,7 @@ struct Options
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;
@@ -162,8 +168,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;
@@ -224,7 +230,7 @@ struct Options
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;
@@ -238,6 +244,10 @@ struct Options
// Override qml import scanner path
QString qmlImportScannerBinaryPath;
bool qmlSkipImportScanning = false;
+ bool buildAar;
+ QString qmlDomBinaryPath;
+ bool generateJavaQmlComponents;
+ QSet<QString> selectedJavaQmlComponents;
};
static const QHash<QByteArray, QByteArray> elfArchitectures = {
@@ -309,23 +319,21 @@ QString fileArchitecture(const Options &options, const QString &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));
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 {};
}
@@ -346,7 +354,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;
}
@@ -526,6 +534,8 @@ Options parseOptions()
options.protectedAuthenticationPath = 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,
@@ -537,6 +547,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;
@@ -559,112 +586,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.\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: Deprecated, ignored.\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: Do not 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,
@@ -731,18 +761,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;
@@ -777,12 +861,16 @@ QString cleanPackageName(QString packageName)
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;
@@ -808,22 +896,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)
@@ -842,7 +943,7 @@ bool readInputFileDirectory(Options *options, QJsonObject &jsonObject, const QSt
if (qtDirectory.isUndefined()) {
for (auto it = options->architectures.constBegin(); it != options->architectures.constEnd(); ++it) {
if (keyName == "qtDataDirectory"_L1) {
- options->architectures[it.key()].qtDirectories[keyName] = it.value().qtInstallDirectory;
+ options->architectures[it.key()].qtDirectories[keyName] = "."_L1;
break;
} else if (keyName == "qtLibsDirectory"_L1) {
options->architectures[it.key()].qtDirectories[keyName] = "lib"_L1;
@@ -1011,6 +1112,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) {
@@ -1196,6 +1302,40 @@ bool readInputFile(Options *options)
}
{
+ const QJsonValue genJavaQmlComponents = jsonObject.value("generate-java-qml-components"_L1);
+ if (!genJavaQmlComponents.isUndefined() && genJavaQmlComponents.isBool()) {
+ options->generateJavaQmlComponents = genJavaQmlComponents.toBool(false);
+ if (options->generateJavaQmlComponents && !options->buildAar) {
+ fprintf(stderr,
+ "Warning: Skipping the generation of Java components from QML as it can be "
+ "enabled only for an AAR target.\n");
+ options->generateJavaQmlComponents = false;
+ }
+ }
+ }
+
+ {
+ const QJsonValue qmlDomBinaryPath = jsonObject.value("qml-dom-binary"_L1);
+ if (!qmlDomBinaryPath.isUndefined()) {
+ options->qmlDomBinaryPath = qmlDomBinaryPath.toString();
+ } else if (options->generateJavaQmlComponents) {
+ fprintf(stderr,
+ "No qmldom binary defined in json file which is required when "
+ "building with QT_ANDROID_GENERATE_JAVA_QML_COMPONENTS flag.\n");
+ return false;
+ }
+ }
+
+ {
+ const QJsonValue qmlFiles = jsonObject.value("qml-files-for-code-generator"_L1);
+ if (!qmlFiles.isUndefined() && qmlFiles.isArray()) {
+ const QJsonArray jArray = qmlFiles.toArray();
+ for (auto &item : jArray)
+ options->selectedJavaQmlComponents << item.toString();
+ }
+ }
+
+ {
const QJsonValue applicationBinary = jsonObject.value("application-binary"_L1);
if (applicationBinary.isUndefined()) {
fprintf(stderr, "No application binary defined in json file.\n");
@@ -1216,6 +1356,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();
@@ -1224,11 +1383,9 @@ 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.size() + 1),
@@ -1240,12 +1397,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));
}
}
}
@@ -1261,9 +1430,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;
}
@@ -1301,7 +1467,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()));
}
}
@@ -1363,6 +1529,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;
}
@@ -1591,27 +1760,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;
}
@@ -1724,16 +1887,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 ||
@@ -1802,6 +1959,11 @@ static QString absoluteFilePath(const Options *options, const QString &relativeF
}
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;
}
@@ -1843,14 +2005,57 @@ QList<QtDependency> findFilesRecursively(const Options &options, const QFileInfo
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 + "/"_L1 + fileName);
- QFileInfo rootPath(options.qtInstallDirectory + "/"_L1);
- return findFilesRecursively(options, info, rootPath.absolutePath() + u'/');
}
bool readAndroidDependencyXml(Options *options,
@@ -1883,29 +2088,15 @@ bool readAndroidDependencyXml(Options *options,
QString file = reader.attributes().value("file"_L1).toString();
- const QList<QtDependency> fileNames = findFilesRecursively(*options, file);
-
- for (const QtDependency &fileName : fileNames) {
- if (usedDependencies->contains(fileName.absolutePath))
- continue;
-
- if (fileName.absolutePath.endsWith(".so"_L1)) {
- QSet<QString> remainingDependencies;
- if (!readDependenciesFromElf(options, fileName.absolutePath,
- usedDependencies,
- &remainingDependencies)) {
- fprintf(stdout, "Skipping dependencies from xml: %s\n",
- qPrintable(fileName.relativePath));
- 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);
+ 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);
+ 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());
@@ -1969,7 +2160,7 @@ QStringList getQtLibsFromElf(const Options &options, const QString &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();
@@ -1979,7 +2170,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();
@@ -2003,8 +2194,6 @@ QStringList getQtLibsFromElf(const Options &options, const QString &fileName)
ret += libraryName;
}
- pclose(readElfCommand);
-
return ret;
}
@@ -2142,7 +2331,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;
@@ -2150,7 +2339,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);
@@ -2294,17 +2483,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;
@@ -2382,6 +2571,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);
@@ -2447,7 +2644,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)) {
@@ -2463,7 +2661,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;
@@ -2554,7 +2752,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;
@@ -2670,11 +2868,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()) {
@@ -2687,24 +2885,6 @@ void checkAndWarnGradleLongPaths(const QString &outputDirectory)
}
#endif
-bool gradleSetsLegacyPackagingProperty(const QString &path)
-{
- QFile file(path);
- if (!file.open(QIODevice::ReadOnly))
- return false;
-
- const auto lines = file.readAll().split('\n');
- for (const auto &line : lines) {
- if (line.contains("useLegacyPackaging")) {
- const auto trimmed = line.trimmed();
- if (!trimmed.startsWith("//") && !trimmed.startsWith('*') && !trimmed.startsWith("/*"))
- return true;
- }
- }
-
- return false;
-}
-
bool buildAndroidProject(const Options &options)
{
GradleProperties localProperties;
@@ -2717,7 +2897,8 @@ bool buildAndroidProject(const Options &options)
GradleProperties gradleProperties = readGradleProperties(gradlePropertiesPath);
const QString gradleBuildFilePath = options.outputDirectory + "build.gradle"_L1;
- if (!gradleSetsLegacyPackagingProperty(gradleBuildFilePath))
+ GradleBuildConfigs gradleConfigs = gradleBuildConfigs(gradleBuildFilePath);
+ if (!gradleConfigs.setsLegacyPackaging)
gradleProperties["android.bundle.enableUncompressedNativeLibs"] = "false";
gradleProperties["buildDir"] = "build";
@@ -2732,7 +2913,27 @@ bool buildAndroidProject(const Options &options)
(options.qtInstallDirectory + u'/' + options.qtDataDirectory +
"/src/android/java"_L1)
.toUtf8();
- gradleProperties["androidCompileSdkVersion"] = options.androidPlatform.split(u'-').last().toLocal8Bit();
+
+ 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();
@@ -2747,6 +2948,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;
@@ -2772,19 +2976,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)
@@ -2810,18 +3014,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)
@@ -2834,39 +3038,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)
@@ -2879,20 +3087,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)
@@ -3010,7 +3218,7 @@ bool signAAB(const Options &options)
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;
@@ -3018,11 +3226,11 @@ bool signAAB(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)
@@ -3050,17 +3258,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 =
@@ -3117,17 +3325,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)
@@ -3171,7 +3379,8 @@ enum ErrorCode
CannotInstallApk = 16,
CannotCopyAndroidExtraResources = 19,
CannotCopyApk = 20,
- CannotCreateRcc = 21
+ CannotCreateRcc = 21,
+ CannotGenerateJavaQmlComponents = 22
};
bool writeDependencyFile(const Options &options)
@@ -3204,6 +3413,362 @@ bool writeDependencyFile(const Options &options)
return true;
}
+int generateJavaQmlComponents(const Options &options)
+{
+ // TODO QTBUG-125892: Current method of path discovery are to be improved
+ // For instance, it does not discover statically linked **inner** QML modules.
+ const auto getImportPaths = [](const QString &buildPath, const QString &libName,
+ QStringList &appImports, QStringList &externalImports) -> bool {
+ QFile confRspFile("/%1/.qt/qml_imports/%2_conf.rsp"_L1.arg(buildPath, libName));
+ if (!confRspFile.exists() || !confRspFile.open(QFile::ReadOnly))
+ return false;
+ QTextStream rspStream(&confRspFile);
+ while (!rspStream.atEnd()) {
+ QString currentLine = rspStream.readLine();
+ if (currentLine.compare("-importPath"_L1) == 0) {
+ currentLine = rspStream.readLine();
+ if (QDir::cleanPath(currentLine).startsWith(QDir::cleanPath(buildPath)))
+ appImports << currentLine;
+ else
+ externalImports << currentLine;
+ }
+ }
+ return appImports.count() + externalImports.count();
+ };
+
+ struct ComponentInfo {
+ QString name;
+ QString path;
+ };
+
+ struct ModuleInfo
+ {
+ QString moduleName;
+ QString preferPath;
+ QList<ComponentInfo> qmlComponents;
+ bool isValid() { return qmlComponents.size() && moduleName.size(); }
+ };
+
+ const auto getModuleInfo = [](const QString &qmldirPath) -> ModuleInfo {
+ QFile qmlDirFile(qmldirPath + "/qmldir"_L1);
+ if (!qmlDirFile.exists() || !qmlDirFile.open(QFile::ReadOnly))
+ return ModuleInfo();
+ ModuleInfo moduleInfo;
+ QTextStream qmldirStream(&qmlDirFile);
+ while (!qmldirStream.atEnd()) {
+ const QString currentLine = qmldirStream.readLine();
+ if (currentLine.size() && currentLine[0].isLower()) {
+ // TODO QTBUG-125891: Handling of QML modules with dotted URI
+ if (currentLine.startsWith("module "_L1))
+ moduleInfo.moduleName = currentLine.split(" "_L1)[1];
+ else if (currentLine.startsWith("prefer "_L1))
+ moduleInfo.preferPath = currentLine.split(" "_L1)[1];
+ } else if (currentLine.size()
+ && (currentLine[0].isUpper() || currentLine.startsWith("singleton"_L1))) {
+ const QStringList parts = currentLine.split(" "_L1);
+ if (parts.size() > 2)
+ moduleInfo.qmlComponents.append({ parts.first(), parts.last() });
+ }
+ }
+ return moduleInfo;
+ };
+
+ const auto extractDomInfo = [](const QString &qmlDomExecPath, const QString &qmldirPath,
+ const QString &qmlFile,
+ const QStringList &otherImportPaths) -> QJsonObject {
+ QByteArray domInfo;
+ QString importFlags;
+ for (auto &importPath : otherImportPaths)
+ importFlags.append(" -i %1"_L1.arg(shellQuote(importPath)));
+
+ const QString qmlDomCmd = "%1 -d -D required -f +:propertyInfos %2 %3"_L1.arg(
+ shellQuote(qmlDomExecPath), importFlags,
+ shellQuote("%1/%2"_L1.arg(qmldirPath, qmlFile)));
+ const QStringList qmlDomCmdParts = QProcess::splitCommand(qmlDomCmd);
+ QProcess process;
+ process.start(qmlDomCmdParts.first(), qmlDomCmdParts.sliced(1));
+ if (!process.waitForStarted()) {
+ fprintf(stderr, "Cannot execute command %s\n", qPrintable(qmlDomCmd));
+ return QJsonObject();
+ }
+ // Wait, maximum 30 seconds
+ if (!process.waitForFinished(30000)) {
+ fprintf(stderr, "Execution of command %s timed out.\n", qPrintable(qmlDomCmd));
+ return QJsonObject();
+ }
+ domInfo = process.readAllStandardOutput();
+
+ QJsonParseError jsonError;
+ const QJsonDocument jsonDoc = QJsonDocument::fromJson(domInfo, &jsonError);
+ if (jsonError.error != QJsonParseError::NoError)
+ fprintf(stderr, "Output of %s is not valid JSON document.", qPrintable(qmlDomCmd));
+ return jsonDoc.object();
+ };
+
+ const auto getComponent = [](const QJsonObject &dom) -> QJsonObject {
+ if (dom.isEmpty())
+ return QJsonObject();
+
+ const QJsonObject currentItem = dom.value("currentItem"_L1).toObject();
+ if (!currentItem.value("isValid"_L1).toBool(false))
+ return QJsonObject();
+
+ const QJsonArray components =
+ currentItem.value("components"_L1).toObject().value(""_L1).toArray();
+ if (components.isEmpty())
+ return QJsonObject();
+ return components.constBegin()->toObject();
+ };
+
+ const auto getProperties = [](const QJsonObject &component) -> QJsonArray {
+ QJsonArray properties;
+ const QJsonArray objects = component.value("objects"_L1).toArray();
+ if (objects.isEmpty())
+ return QJsonArray();
+ const QJsonObject propertiesObject =
+ objects[0].toObject().value("propertyInfos"_L1).toObject();
+ for (const auto &jsonProperty : propertiesObject) {
+ const QJsonArray propertyDefs =
+ jsonProperty.toObject().value("propertyDefs"_L1).toArray();
+ if (propertyDefs.isEmpty())
+ continue;
+
+ properties.append(propertyDefs[0].toObject());
+ }
+ return properties;
+ };
+
+ const auto getMethods = [](const QJsonObject &component) -> QJsonArray {
+ QJsonArray methods;
+ const QJsonArray objects = component.value("objects"_L1).toArray();
+ if (objects.isEmpty())
+ return QJsonArray();
+ const QJsonObject methodsObject = objects[0].toObject().value("methods"_L1).toObject();
+ for (const auto &jsonMethod : methodsObject) {
+ const QJsonArray overloads = jsonMethod.toArray();
+ for (const auto &m : overloads)
+ methods.append(m);
+ }
+ return methods;
+ };
+
+ const static QHash<QString, QString> qmlToJavaType = {
+ { "qreal"_L1, "Double"_L1 }, { "double"_L1, "Double"_L1 }, { "int"_L1, "Integer"_L1 },
+ { "float"_L1, "Float"_L1 }, { "bool"_L1, "Boolean"_L1 }, { "string"_L1, "String"_L1 },
+ { "void"_L1, "Void"_L1 }
+ };
+
+ const auto endBlock = [](QTextStream &stream, int indentWidth = 0) {
+ stream << QString(indentWidth, u' ') << "}\n";
+ };
+
+ const auto createHeaderBlock = [](QTextStream &stream, const QString &javaPackage) {
+ stream << "/* This file is autogenerated by androiddeployqt. Do not edit */\n\n"
+ << "package %1;\n\n"_L1.arg(javaPackage)
+ << "import org.qtproject.qt.android.QtSignalListener;\n"
+ << "import org.qtproject.qt.android.QtQmlComponent;\n\n";
+ };
+
+ const auto beginLibraryBlock = [](QTextStream &stream, const QString &libName) {
+ stream << QLatin1StringView("public final class %1 {\n").arg(libName);
+ };
+
+ const auto beginModuleBlock = [](QTextStream &stream, const QString &moduleName,
+ bool topLevel = false, int indentWidth = 4) {
+ const QString indent(indentWidth, u' ');
+ stream << indent
+ << "public final%1 class %2 {\n"_L1.arg(topLevel ? ""_L1 : " static"_L1, moduleName);
+ };
+
+ const auto beginComponentBlock = [](QTextStream &stream, const QString &libName,
+ const QString &moduleName, const QString &preferPath,
+ const ComponentInfo &componentInfo, int indentWidth = 8) {
+ const QString indent(indentWidth, u' ');
+
+ stream << indent
+ << "public final static class %1 extends QtQmlComponent {\n"_L1
+ .arg(componentInfo.name)
+ << indent << " @Override public String getLibraryName() {\n"_L1
+ << indent << " return \"%1\";\n"_L1.arg(libName)
+ << indent << " }\n"_L1
+ << indent << " @Override public String getModuleName() {\n"_L1
+ << indent << " return \"%1\";\n"_L1.arg(moduleName)
+ << indent << " }\n"_L1
+ << indent << " @Override public String getFilePath() {\n"_L1
+ << indent << " return \"qrc%1%2\";\n"_L1.arg(preferPath)
+ .arg(componentInfo.path)
+ << indent << " }\n"_L1;
+ };
+
+ const auto beginPropertyBlock = [](QTextStream &stream, const QJsonObject &propertyData,
+ int indentWidth = 8) {
+ const QString indent(indentWidth, u' ');
+ const QString propertyName = propertyData["name"_L1].toString();
+ if (propertyName.isEmpty())
+ return;
+ const QString upperPropertyName =
+ propertyName[0].toUpper() + propertyName.last(propertyName.size() - 1);
+ const QString typeName = propertyData["typeName"_L1].toString();
+ const bool isReadyonly = propertyData["isReadonly"_L1].toBool();
+
+ const QString javaTypeName = qmlToJavaType.value(typeName, "Object"_L1);
+
+ if (!isReadyonly) {
+ stream << indent
+ << "public void set%1(%2 %3) { setProperty(\"%3\", %3); }\n"_L1.arg(
+ upperPropertyName, javaTypeName, propertyName);
+ }
+
+ stream << indent
+ << "public %2 get%1() { return this.<%2>getProperty(\"%3\"); }\n"_L1
+ .arg(upperPropertyName, javaTypeName, propertyName)
+ << indent
+ << "public int connect%1ChangeListener(QtSignalListener<%2> signalListener) {\n"_L1
+ .arg(upperPropertyName, javaTypeName)
+ << indent
+ << " return connectSignalListener(\"%1\", %2.class, signalListener);\n"_L1.arg(
+ propertyName, javaTypeName)
+ << indent << "}\n";
+ };
+
+ const auto beginSignalBlock = [](QTextStream &stream, const QJsonObject &methodData,
+ int indentWidth = 8) {
+ const QString indent(indentWidth, u' ');
+ if (methodData["methodType"_L1] != 0)
+ return;
+ const QJsonArray parameters = methodData["parameters"_L1].toArray();
+ if (parameters.size() > 1)
+ return;
+
+ const QString methodName = methodData["name"_L1].toString();
+ if (methodName.isEmpty())
+ return;
+ const QString upperMethodName =
+ methodName[0].toUpper() + methodName.last(methodName.size() - 1);
+ const QString typeName = !parameters.isEmpty()
+ ? parameters[0].toObject()["typeName"_L1].toString()
+ : "void"_L1;
+
+ const QString javaTypeName = qmlToJavaType.value(typeName, "Object"_L1);
+ stream << indent
+ << "public int connect%1Listener(QtSignalListener<%2> signalListener) {\n"_L1.arg(
+ upperMethodName, javaTypeName)
+ << indent
+ << " return connectSignalListener(\"%1\", %2.class, signalListener);\n"_L1.arg(
+ methodName, javaTypeName)
+ << indent << "}\n";
+ };
+
+ const QString libName(options.applicationBinary);
+ const QString libClassname = libName[0].toUpper() + libName.last(libName.size() - 1);
+ const QString javaPackage = options.packageName;
+ const QString outputDir = "%1/src/%2"_L1.arg(options.outputDirectory,
+ QString(javaPackage).replace(u'.', u'/'));
+ const QString buildPath(QDir(options.buildDirectory).absolutePath());
+ const QString domBinaryPath(options.qmlDomBinaryPath);
+ const bool leafEqualsLibname = javaPackage.endsWith(".%1"_L1.arg(libName));
+
+ fprintf(stdout, "Generating Java QML Components in %s directory.\n", qPrintable(outputDir));
+ if (!QDir().current().mkpath(outputDir)) {
+ fprintf(stderr, "Cannot create %s directory\n", qPrintable(outputDir));
+ return false;
+ }
+
+ QStringList appImports;
+ QStringList externalImports;
+ if (!getImportPaths(buildPath, libName, appImports, externalImports))
+ return false;
+
+ QTextStream outputStream;
+ std::unique_ptr<QFile> outputFile;
+
+ if (!leafEqualsLibname) {
+ outputFile.reset(new QFile("%1/%2.java"_L1.arg(outputDir, libClassname)));
+ if (outputFile->exists())
+ outputFile->remove();
+ if (!outputFile->open(QFile::ReadWrite)) {
+ fprintf(stderr, "Cannot open %s file to write.\n",
+ qPrintable(outputFile->fileName()));
+ return false;
+ }
+ outputStream.setDevice(outputFile.get());
+ createHeaderBlock(outputStream, javaPackage);
+ beginLibraryBlock(outputStream, libClassname);
+ }
+
+ int generatedComponents = 0;
+ for (const auto &importPath : appImports) {
+ ModuleInfo moduleInfo = getModuleInfo(importPath);
+ if (!moduleInfo.isValid())
+ continue;
+
+ const QString moduleClassname = moduleInfo.moduleName[0].toUpper()
+ + moduleInfo.moduleName.last(moduleInfo.moduleName.size() - 1);
+
+ int indentBase = 4;
+ if (leafEqualsLibname) {
+ indentBase = 0;
+ QIODevice *outputStreamDevice = outputStream.device();
+ if (outputStreamDevice) {
+ outputStream.flush();
+ outputStream.reset();
+ outputStreamDevice->close();
+ }
+
+ outputFile.reset(new QFile("%1/%2.java"_L1.arg(outputDir,moduleClassname)));
+ if (outputFile->exists() && !outputFile->remove())
+ return false;
+ if (!outputFile->open(QFile::ReadWrite)) {
+ fprintf(stderr, "Cannot open %s file to write.\n", qPrintable(outputFile->fileName()));
+ return false;
+ }
+
+ outputStream.setDevice(outputFile.get());
+ createHeaderBlock(outputStream, javaPackage);
+ }
+
+ beginModuleBlock(outputStream, moduleClassname, leafEqualsLibname, indentBase);
+ indentBase += 4;
+
+ for (const auto &qmlComponent : moduleInfo.qmlComponents) {
+ const bool isSelected = options.selectedJavaQmlComponents.contains(
+ "%1.%2"_L1.arg(moduleInfo.moduleName, qmlComponent.name));
+ if (!options.selectedJavaQmlComponents.isEmpty() && !isSelected)
+ continue;
+
+ QJsonObject domInfo = extractDomInfo(domBinaryPath, importPath, qmlComponent.path,
+ externalImports + appImports);
+ QJsonObject component = getComponent(domInfo);
+ if (component.isEmpty())
+ continue;
+
+ beginComponentBlock(outputStream, libName, moduleInfo.moduleName, moduleInfo.preferPath,
+ qmlComponent, indentBase);
+ indentBase += 4;
+
+ const QJsonArray properties = getProperties(component);
+ for (const QJsonValue &p : std::as_const(properties))
+ beginPropertyBlock(outputStream, p.toObject(), indentBase);
+
+ const QJsonArray methods = getMethods(component);
+ for (const QJsonValue &m : std::as_const(methods))
+ beginSignalBlock(outputStream, m.toObject(), indentBase);
+
+ indentBase -= 4;
+ endBlock(outputStream, indentBase);
+ generatedComponents++;
+ }
+ indentBase -= 4;
+ endBlock(outputStream, indentBase);
+ }
+ if (!leafEqualsLibname)
+ endBlock(outputStream, 0);
+
+ outputStream.flush();
+ outputStream.device()->close();
+ return generatedComponents;
+}
+
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
@@ -3248,7 +3813,7 @@ int main(int argc, char *argv[])
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());
@@ -3285,13 +3850,22 @@ 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 (options.generateJavaQmlComponents) {
+ if (!generateJavaQmlComponents(options))
+ return CannotGenerateJavaQmlComponents;
}
+
+ if (Q_UNLIKELY(options.timing)) {
+ fprintf(stdout, "[TIMING] %lld ns: Generate Java QtQmlComponents.\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.