diff options
Diffstat (limited to 'src/tools/androiddeployqt/main.cpp')
-rw-r--r-- | src/tools/androiddeployqt/main.cpp | 1292 |
1 files changed, 990 insertions, 302 deletions
diff --git a/src/tools/androiddeployqt/main.cpp b/src/tools/androiddeployqt/main.cpp index 787c2f405c..f9a645aadb 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 @@ -70,10 +72,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; }; @@ -96,6 +106,9 @@ struct Options , installApk(false) , uninstallApk(false) , qmlImportScannerBinaryPath() + , buildAar(false) + , qmlDomBinaryPath() + , generateJavaQmlComponents(false) {} enum DeploymentMechanism @@ -128,8 +141,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; @@ -148,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; @@ -198,12 +218,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; @@ -217,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 = { @@ -288,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 {}; } @@ -325,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; } @@ -505,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, @@ -516,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; @@ -538,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, @@ -710,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; @@ -756,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; @@ -787,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) @@ -815,6 +937,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); @@ -871,7 +1053,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; } @@ -899,6 +1082,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()) { @@ -921,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) { @@ -1106,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"); @@ -1126,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(); @@ -1134,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), @@ -1150,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)); } } } @@ -1171,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; } @@ -1211,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())); } } @@ -1220,13 +1476,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; @@ -1244,7 +1502,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; @@ -1270,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; } @@ -1369,7 +1631,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); @@ -1497,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; } @@ -1630,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 || @@ -1706,6 +1957,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; } @@ -1734,13 +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 + u'/' + fileName); - return findFilesRecursively(options, info, options.qtInstallDirectory + u'/'); } bool readAndroidDependencyXml(Options *options, @@ -1748,7 +2063,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()) { @@ -1773,28 +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()); @@ -1858,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(); @@ -1868,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(); @@ -1892,8 +2194,6 @@ QStringList getQtLibsFromElf(const Options &options, const QString &fileName) ret += libraryName; } - pclose(readElfCommand); - return ret; } @@ -1951,8 +2251,8 @@ bool scanImports(Options *options, QSet<QString> *usedDependencies) if (!options->qmlImportScannerBinaryPath.isEmpty()) { qmlImportScanner = options->qmlImportScannerBinaryPath; } else { - qmlImportScanner = execSuffixAppended(options->qtInstallDirectory + u'/' - + defaultLibexecDir() + "/qmlimportscanner"_L1); + qmlImportScanner = execSuffixAppended(options->qtLibExecsDirectory + + "/qmlimportscanner"_L1); } QStringList importPaths; @@ -1961,7 +2261,7 @@ bool scanImports(Options *options, QSet<QString> *usedDependencies) // lacks a qml directory. We don't want to pass it as an import path if it doesn't exist // because it will cause qmlimportscanner to fail. // This also covers the case when only qtbase is installed in a regular Qt build. - const QString mainImportPath = options->qtInstallDirectory + "/qml"_L1; + const QString mainImportPath = options->qtInstallDirectory + u'/' + options->qtQmlDirectory; if (QFile::exists(mainImportPath)) importPaths += shellQuote(mainImportPath); @@ -2031,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; @@ -2039,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); @@ -2183,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; @@ -2215,8 +2515,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)) { @@ -2272,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); @@ -2337,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)) { @@ -2353,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; @@ -2409,12 +2717,8 @@ bool copyQtFiles(Options *options) 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); @@ -2448,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; @@ -2564,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()) { @@ -2581,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; @@ -2611,16 +2897,43 @@ 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"; - 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(); @@ -2635,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; @@ -2660,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) @@ -2698,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) @@ -2722,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) @@ -2767,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) @@ -2898,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; @@ -2906,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) @@ -2938,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 = @@ -3005,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) @@ -3059,7 +3379,8 @@ enum ErrorCode CannotInstallApk = 16, CannotCopyAndroidExtraResources = 19, CannotCopyApk = 20, - CannotCreateRcc = 21 + CannotCreateRcc = 21, + CannotGenerateJavaQmlComponents = 22 }; bool writeDependencyFile(const Options &options) @@ -3092,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); @@ -3131,10 +3808,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()); @@ -3171,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. |