1// Copyright (C) 2021 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3
4#include <QCoreApplication>
5#include <QStringList>
6#include <QDir>
7#include <QDirIterator>
8#include <QJsonDocument>
9#include <QJsonObject>
10#include <QJsonArray>
11#include <QJsonValue>
12#include <QDebug>
13#include <QDataStream>
14#include <QXmlStreamReader>
15#include <QStandardPaths>
16#include <QUuid>
17#include <QDirListing>
18#include <QElapsedTimer>
19#include <QRegularExpression>
20#include <QSettings>
21#include <QHash>
22#include <QSet>
23#include <QMap>
24#if QT_CONFIG(process)
25#include <QProcess>
26#endif
27
28#include <depfile_shared.h>
29#include <shellquote_shared.h>
30
31#include <algorithm>
32
33#if defined(Q_OS_WIN32)
34#include <qt_windows.h>
35#endif
36
37#ifdef Q_CC_MSVC
38#define popen _popen
39#define QT_POPEN_READ "rb"
40#define pclose _pclose
41#else
42#define QT_POPEN_READ "r"
43#endif
44
45using namespace Qt::StringLiterals;
46
47static const bool mustReadOutputAnyway = true; // pclose seems to return the wrong error code unless we read the output
48
49static QStringList dependenciesForDepfile;
50
51auto openProcess(const QString &command)
52{
53#if defined(Q_OS_WIN32)
54 QString processedCommand = u'\"' + command + u'\"';
55#else
56 const QString& processedCommand = command;
57#endif
58 struct Closer { void operator()(FILE *proc) const { if (proc) (void)pclose(stream: proc); } };
59 using UP = std::unique_ptr<FILE, Closer>;
60 return UP{popen(command: processedCommand.toLocal8Bit().constData(), QT_POPEN_READ)};
61}
62
63struct QtDependency
64{
65 QtDependency(const QString &rpath, const QString &apath) : relativePath(rpath), absolutePath(apath) {}
66
67 bool operator==(const QtDependency &other) const
68 {
69 return relativePath == other.relativePath && absolutePath == other.absolutePath;
70 }
71
72 QString relativePath;
73 QString absolutePath;
74};
75
76struct QtInstallDirectoryWithTriple
77{
78 QtInstallDirectoryWithTriple(const QString &dir = QString(),
79 const QString &t = QString(),
80 const QHash<QString, QString> &dirs = QHash<QString, QString>()
81 ) :
82 qtInstallDirectory(dir),
83 qtDirectories(dirs),
84 triple(t),
85 enabled(false)
86 {}
87
88 QString qtInstallDirectory;
89 QHash<QString, QString> qtDirectories;
90 QString triple;
91 bool enabled;
92};
93
94struct Options
95{
96 Options()
97 : helpRequested(false)
98 , verbose(false)
99 , timing(false)
100 , build(true)
101 , auxMode(false)
102 , deploymentMechanism(Bundled)
103 , releasePackage(false)
104 , digestAlg("SHA-256"_L1)
105 , sigAlg("SHA256withRSA"_L1)
106 , internalSf(false)
107 , sectionsOnly(false)
108 , protectedAuthenticationPath(false)
109 , installApk(false)
110 , uninstallApk(false)
111 , qmlImportScannerBinaryPath()
112 , buildAar(false)
113 , qmlDomBinaryPath()
114 , generateJavaQmlComponents(false)
115 {}
116
117 enum DeploymentMechanism
118 {
119 Bundled,
120 Unbundled
121 };
122
123 enum TriState {
124 Auto,
125 False,
126 True
127 };
128
129 bool helpRequested;
130 bool verbose;
131 bool timing;
132 bool build;
133 bool auxMode;
134 bool noRccBundleCleanup = false;
135 bool copyDependenciesOnly = false;
136 QElapsedTimer timer;
137
138 // External tools
139 QString sdkPath;
140 QString sdkBuildToolsVersion;
141 QString ndkPath;
142 QString ndkVersion;
143 QString jdkPath;
144
145 // Build paths
146 QString qtInstallDirectory;
147 QHash<QString, QString> qtDirectories;
148 QString qtDataDirectory;
149 QString qtLibsDirectory;
150 QString qtLibExecsDirectory;
151 QString qtPluginsDirectory;
152 QString qtQmlDirectory;
153 QString qtHostDirectory;
154 std::vector<QString> extraPrefixDirs;
155 QStringList androidDeployPlugins;
156 // Unlike 'extraPrefixDirs', the 'extraLibraryDirs' key doesn't expect the 'lib' subfolder
157 // when looking for dependencies.
158 std::vector<QString> extraLibraryDirs;
159 QString androidSourceDirectory;
160 QString outputDirectory;
161 QString inputFileName;
162 QString applicationBinary;
163 QString applicationArguments;
164 std::vector<QString> rootPaths;
165 QString rccBinaryPath;
166 QString depFilePath;
167 QString buildDirectory;
168 QStringList qmlImportPaths;
169 QStringList qrcFiles;
170
171 // Versioning
172 QString versionName;
173 QString versionCode;
174 QByteArray minSdkVersion{"28"};
175 QByteArray targetSdkVersion{"34"};
176
177 // lib c++ path
178 QString stdCppPath;
179 QString stdCppName = QStringLiteral("c++_shared");
180
181 // Build information
182 QString androidPlatform;
183 QHash<QString, QtInstallDirectoryWithTriple> architectures;
184 QString currentArchitecture;
185 QString toolchainPrefix;
186 QString ndkHost;
187 bool buildAAB = false;
188 bool isZstdCompressionEnabled = false;
189
190
191 // Package information
192 DeploymentMechanism deploymentMechanism;
193 QString systemLibsPath;
194 QString packageName;
195 QStringList extraLibs;
196 QHash<QString, QStringList> archExtraLibs;
197 QStringList extraPlugins;
198 QHash<QString, QStringList> archExtraPlugins;
199
200 // Signing information
201 bool releasePackage;
202 QString keyStore;
203 QString keyStorePassword;
204 QString keyStoreAlias;
205 QString storeType;
206 QString keyPass;
207 QString sigFile;
208 QString signedJar;
209 QString digestAlg;
210 QString sigAlg;
211 QString tsaUrl;
212 QString tsaCert;
213 bool internalSf;
214 bool sectionsOnly;
215 bool protectedAuthenticationPath;
216 QString apkPath;
217
218 // Installation information
219 bool installApk;
220 bool uninstallApk;
221 QString installLocation;
222
223 // Per architecture collected information
224 void setCurrentQtArchitecture(const QString &arch,
225 const QString &directory,
226 const QHash<QString, QString> &directories)
227 {
228 currentArchitecture = arch;
229 qtInstallDirectory = directory;
230 qtDataDirectory = directories["qtDataDirectory"_L1];
231 qtLibsDirectory = directories["qtLibsDirectory"_L1];
232 qtLibExecsDirectory = directories["qtLibExecsDirectory"_L1];
233 qtPluginsDirectory = directories["qtPluginsDirectory"_L1];
234 qtQmlDirectory = directories["qtQmlDirectory"_L1];
235 }
236 using BundledFile = std::pair<QString, QString>;
237 QHash<QString, QList<BundledFile>> bundledFiles;
238 QHash<QString, QList<QtDependency>> qtDependencies;
239 QHash<QString, QStringList> localLibs;
240 bool usesOpenGL = false;
241
242 // Per package collected information
243 QStringList initClasses;
244 // permissions 'name' => 'optional additional attributes'
245 QMap<QString, QString> permissions;
246 QStringList features;
247
248 // Override qml import scanner path
249 QString qmlImportScannerBinaryPath;
250 bool qmlSkipImportScanning = false;
251 bool buildAar;
252 QString qmlDomBinaryPath;
253 bool generateJavaQmlComponents;
254 QSet<QString> selectedJavaQmlComponents;
255};
256
257static const QHash<QByteArray, QByteArray> elfArchitectures = {
258 {"aarch64", "arm64-v8a"},
259 {"arm", "armeabi-v7a"},
260 {"i386", "x86"},
261 {"x86_64", "x86_64"}
262};
263
264bool goodToCopy(const Options *options, const QString &file, QStringList *unmetDependencies);
265bool checkCanImportFromRootPaths(const Options *options, const QString &absolutePath,
266 const QString &moduleUrl);
267bool readDependenciesFromElf(Options *options, const QString &fileName,
268 QSet<QString> *usedDependencies, QSet<QString> *remainingDependencies);
269
270QString architectureFromName(const QString &name)
271{
272 QRegularExpression architecture(QStringLiteral("_(armeabi-v7a|arm64-v8a|x86|x86_64).so$"));
273 auto match = architecture.match(subject: name);
274 if (!match.hasMatch())
275 return {};
276 return match.captured(nth: 1);
277}
278
279static QString execSuffixAppended(QString path)
280{
281#if defined(Q_OS_WIN32)
282 path += ".exe"_L1;
283#endif
284 return path;
285}
286
287static QString batSuffixAppended(QString path)
288{
289#if defined(Q_OS_WIN32)
290 path += ".bat"_L1;
291#endif
292 return path;
293}
294
295QString defaultLibexecDir()
296{
297#ifdef Q_OS_WIN32
298 return "bin"_L1;
299#else
300 return "libexec"_L1;
301#endif
302}
303
304static QString llvmReadobjPath(const Options &options)
305{
306 return execSuffixAppended(path: "%1/toolchains/%2/prebuilt/%3/bin/llvm-readobj"_L1
307 .arg(args: options.ndkPath,
308 args: options.toolchainPrefix,
309 args: options.ndkHost));
310}
311
312QString fileArchitecture(const Options &options, const QString &path)
313{
314 auto arch = architectureFromName(name: path);
315 if (!arch.isEmpty())
316 return arch;
317
318 QString readElf = llvmReadobjPath(options);
319 if (!QFile::exists(fileName: readElf)) {
320 fprintf(stderr, format: "Command does not exist: %s\n", qPrintable(readElf));
321 return {};
322 }
323
324 readElf = "%1 --needed-libs %2"_L1.arg(args: shellQuote(arg: readElf), args: shellQuote(arg: path));
325
326 auto readElfCommand = openProcess(command: readElf);
327 if (!readElfCommand) {
328 fprintf(stderr, format: "Cannot execute command %s\n", qPrintable(readElf));
329 return {};
330 }
331
332 char buffer[512];
333 while (fgets(s: buffer, n: sizeof(buffer), stream: readElfCommand.get()) != nullptr) {
334 QByteArray line = QByteArray::fromRawData(data: buffer, size: qstrlen(str: buffer));
335 line = line.trimmed();
336 if (line.startsWith(bv: "Arch: ")) {
337 auto it = elfArchitectures.find(key: line.mid(index: 6));
338 return it != elfArchitectures.constEnd() ? QString::fromLatin1(ba: it.value()) : QString{};
339 }
340 }
341 return {};
342}
343
344bool checkArchitecture(const Options &options, const QString &fileName)
345{
346 return fileArchitecture(options, path: fileName) == options.currentArchitecture;
347}
348
349void deleteMissingFiles(const Options &options, const QDir &srcDir, const QDir &dstDir)
350{
351 if (options.verbose)
352 fprintf(stdout, format: "Delete missing files %s %s\n", qPrintable(srcDir.absolutePath()), qPrintable(dstDir.absolutePath()));
353
354 const QFileInfoList srcEntries = srcDir.entryInfoList(filters: QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs);
355 const QFileInfoList dstEntries = dstDir.entryInfoList(filters: QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs);
356 for (const QFileInfo &dst : dstEntries) {
357 bool found = false;
358 for (const QFileInfo &src : srcEntries)
359 if (dst.fileName() == src.fileName()) {
360 if (dst.isDir())
361 deleteMissingFiles(options, srcDir: src.absoluteFilePath(), dstDir: dst.absoluteFilePath());
362 found = true;
363 break;
364 }
365
366 if (!found) {
367 if (options.verbose)
368 fprintf(stdout, format: "%s not found in %s, removing it.\n", qPrintable(dst.fileName()), qPrintable(srcDir.absolutePath()));
369
370 if (dst.isDir())
371 QDir{dst.absolutePath()}.removeRecursively();
372 else
373 QFile::remove(fileName: dst.absoluteFilePath());
374 }
375 }
376 fflush(stdout);
377}
378
379Options parseOptions()
380{
381 Options options;
382
383 QStringList arguments = QCoreApplication::arguments();
384 for (int i=0; i<arguments.size(); ++i) {
385 const QString &argument = arguments.at(i);
386 if (argument.compare(other: "--output"_L1, cs: Qt::CaseInsensitive) == 0) {
387 if (i + 1 == arguments.size())
388 options.helpRequested = true;
389 else
390 options.outputDirectory = arguments.at(i: ++i).trimmed();
391 } else if (argument.compare(other: "--input"_L1, cs: Qt::CaseInsensitive) == 0) {
392 if (i + 1 == arguments.size())
393 options.helpRequested = true;
394 else
395 options.inputFileName = arguments.at(i: ++i);
396 } else if (argument.compare(other: "--aab"_L1, cs: Qt::CaseInsensitive) == 0) {
397 options.buildAAB = true;
398 options.build = true;
399 } else if (!options.buildAAB && argument.compare(other: "--no-build"_L1, cs: Qt::CaseInsensitive) == 0) {
400 options.build = false;
401 } else if (argument.compare(other: "--install"_L1, cs: Qt::CaseInsensitive) == 0) {
402 options.installApk = true;
403 options.uninstallApk = true;
404 } else if (argument.compare(other: "--reinstall"_L1, cs: Qt::CaseInsensitive) == 0) {
405 options.installApk = true;
406 options.uninstallApk = false;
407 } else if (argument.compare(other: "--android-platform"_L1, cs: Qt::CaseInsensitive) == 0) {
408 if (i + 1 == arguments.size())
409 options.helpRequested = true;
410 else
411 options.androidPlatform = arguments.at(i: ++i);
412 } else if (argument.compare(other: "--help"_L1, cs: Qt::CaseInsensitive) == 0) {
413 options.helpRequested = true;
414 } else if (argument.compare(other: "--verbose"_L1, cs: Qt::CaseInsensitive) == 0) {
415 options.verbose = true;
416 } else if (argument.compare(other: "--deployment"_L1, cs: Qt::CaseInsensitive) == 0) {
417 if (i + 1 == arguments.size()) {
418 options.helpRequested = true;
419 } else {
420 QString deploymentMechanism = arguments.at(i: ++i);
421 if (deploymentMechanism.compare(other: "bundled"_L1, cs: Qt::CaseInsensitive) == 0) {
422 options.deploymentMechanism = Options::Bundled;
423 } else if (deploymentMechanism.compare(other: "unbundled"_L1,
424 cs: Qt::CaseInsensitive) == 0) {
425 options.deploymentMechanism = Options::Unbundled;
426 } else {
427 fprintf(stderr, format: "Unrecognized deployment mechanism: %s\n", qPrintable(deploymentMechanism));
428 options.helpRequested = true;
429 }
430 }
431 } else if (argument.compare(other: "--device"_L1, cs: Qt::CaseInsensitive) == 0) {
432 if (i + 1 == arguments.size())
433 options.helpRequested = true;
434 else
435 options.installLocation = arguments.at(i: ++i);
436 } else if (argument.compare(other: "--release"_L1, cs: Qt::CaseInsensitive) == 0) {
437 options.releasePackage = true;
438 } else if (argument.compare(other: "--jdk"_L1, cs: Qt::CaseInsensitive) == 0) {
439 if (i + 1 == arguments.size())
440 options.helpRequested = true;
441 else
442 options.jdkPath = arguments.at(i: ++i);
443 } else if (argument.compare(other: "--apk"_L1, cs: Qt::CaseInsensitive) == 0) {
444 if (i + 1 == arguments.size())
445 options.helpRequested = true;
446 else
447 options.apkPath = arguments.at(i: ++i);
448 } else if (argument.compare(other: "--depfile"_L1, cs: Qt::CaseInsensitive) == 0) {
449 if (i + 1 == arguments.size())
450 options.helpRequested = true;
451 else
452 options.depFilePath = arguments.at(i: ++i);
453 } else if (argument.compare(other: "--builddir"_L1, cs: Qt::CaseInsensitive) == 0) {
454 if (i + 1 == arguments.size())
455 options.helpRequested = true;
456 else
457 options.buildDirectory = arguments.at(i: ++i);
458 } else if (argument.compare(other: "--sign"_L1, cs: Qt::CaseInsensitive) == 0) {
459 if (i + 2 < arguments.size() && !arguments.at(i: i + 1).startsWith(s: "--"_L1) &&
460 !arguments.at(i: i + 2).startsWith(s: "--"_L1)) {
461 options.keyStore = arguments.at(i: ++i);
462 options.keyStoreAlias = arguments.at(i: ++i);
463 } else {
464 const QString keyStore = qEnvironmentVariable(varName: "QT_ANDROID_KEYSTORE_PATH");
465 const QString storeAlias = qEnvironmentVariable(varName: "QT_ANDROID_KEYSTORE_ALIAS");
466 if (keyStore.isEmpty() || storeAlias.isEmpty()) {
467 options.helpRequested = true;
468 fprintf(stderr, format: "Package signing path and alias values are not specified.\n");
469 } else {
470 fprintf(stdout,
471 format: "Using package signing path and alias values found from the "
472 "environment variables.\n");
473 options.keyStore = keyStore;
474 options.keyStoreAlias = storeAlias;
475 }
476 }
477
478 // Do not override if the passwords are provided through arguments
479 if (options.keyStorePassword.isEmpty()) {
480 fprintf(stdout, format: "Using package signing store password found from the environment "
481 "variable.\n");
482 options.keyStorePassword = qEnvironmentVariable(varName: "QT_ANDROID_KEYSTORE_STORE_PASS");
483 }
484 if (options.keyPass.isEmpty()) {
485 fprintf(stdout, format: "Using package signing key password found from the environment "
486 "variable.\n");
487 options.keyPass = qEnvironmentVariable(varName: "QT_ANDROID_KEYSTORE_KEY_PASS");
488 }
489 } else if (argument.compare(other: "--storepass"_L1, cs: Qt::CaseInsensitive) == 0) {
490 if (i + 1 == arguments.size())
491 options.helpRequested = true;
492 else
493 options.keyStorePassword = arguments.at(i: ++i);
494 } else if (argument.compare(other: "--storetype"_L1, cs: Qt::CaseInsensitive) == 0) {
495 if (i + 1 == arguments.size())
496 options.helpRequested = true;
497 else
498 options.storeType = arguments.at(i: ++i);
499 } else if (argument.compare(other: "--keypass"_L1, cs: Qt::CaseInsensitive) == 0) {
500 if (i + 1 == arguments.size())
501 options.helpRequested = true;
502 else
503 options.keyPass = arguments.at(i: ++i);
504 } else if (argument.compare(other: "--sigfile"_L1, cs: Qt::CaseInsensitive) == 0) {
505 if (i + 1 == arguments.size())
506 options.helpRequested = true;
507 else
508 options.sigFile = arguments.at(i: ++i);
509 } else if (argument.compare(other: "--digestalg"_L1, cs: Qt::CaseInsensitive) == 0) {
510 if (i + 1 == arguments.size())
511 options.helpRequested = true;
512 else
513 options.digestAlg = arguments.at(i: ++i);
514 } else if (argument.compare(other: "--sigalg"_L1, cs: Qt::CaseInsensitive) == 0) {
515 if (i + 1 == arguments.size())
516 options.helpRequested = true;
517 else
518 options.sigAlg = arguments.at(i: ++i);
519 } else if (argument.compare(other: "--tsa"_L1, cs: Qt::CaseInsensitive) == 0) {
520 if (i + 1 == arguments.size())
521 options.helpRequested = true;
522 else
523 options.tsaUrl = arguments.at(i: ++i);
524 } else if (argument.compare(other: "--tsacert"_L1, cs: Qt::CaseInsensitive) == 0) {
525 if (i + 1 == arguments.size())
526 options.helpRequested = true;
527 else
528 options.tsaCert = arguments.at(i: ++i);
529 } else if (argument.compare(other: "--internalsf"_L1, cs: Qt::CaseInsensitive) == 0) {
530 options.internalSf = true;
531 } else if (argument.compare(other: "--sectionsonly"_L1, cs: Qt::CaseInsensitive) == 0) {
532 options.sectionsOnly = true;
533 } else if (argument.compare(other: "--protected"_L1, cs: Qt::CaseInsensitive) == 0) {
534 options.protectedAuthenticationPath = true;
535 } else if (argument.compare(other: "--aux-mode"_L1, cs: Qt::CaseInsensitive) == 0) {
536 options.auxMode = true;
537 } else if (argument.compare(other: "--build-aar"_L1, cs: Qt::CaseInsensitive) == 0) {
538 options.buildAar = true;
539 } else if (argument.compare(other: "--qml-importscanner-binary"_L1, cs: Qt::CaseInsensitive) == 0) {
540 options.qmlImportScannerBinaryPath = arguments.at(i: ++i).trimmed();
541 } else if (argument.compare(other: "--no-rcc-bundle-cleanup"_L1,
542 cs: Qt::CaseInsensitive) == 0) {
543 options.noRccBundleCleanup = true;
544 } else if (argument.compare(other: "--copy-dependencies-only"_L1,
545 cs: Qt::CaseInsensitive) == 0) {
546 options.copyDependenciesOnly = true;
547 }
548 }
549
550 if (options.buildAar) {
551 if (options.installApk || options.uninstallApk) {
552 fprintf(stderr, format: "Warning: Skipping %s, AAR packages are not installable.\n",
553 options.uninstallApk ? "--reinstall" : "--install");
554 options.installApk = false;
555 options.uninstallApk = false;
556 }
557 if (options.buildAAB) {
558 fprintf(stderr, format: "Warning: Skipping -aab as --build-aar is present.\n");
559 options.buildAAB = false;
560 }
561 if (!options.keyStore.isEmpty()) {
562 fprintf(stderr, format: "Warning: Skipping --sign, signing AAR packages is not supported.\n");
563 options.keyStore.clear();
564 }
565 }
566
567 if (options.buildDirectory.isEmpty() && !options.depFilePath.isEmpty())
568 options.helpRequested = true;
569
570 if (options.inputFileName.isEmpty())
571 options.inputFileName = "android-%1-deployment-settings.json"_L1.arg(args: QDir::current().dirName());
572
573 options.timing = qEnvironmentVariableIsSet(varName: "ANDROIDDEPLOYQT_TIMING_OUTPUT");
574
575 if (!QDir::current().mkpath(dirPath: options.outputDirectory)) {
576 fprintf(stderr, format: "Invalid output directory: %s\n", qPrintable(options.outputDirectory));
577 options.outputDirectory.clear();
578 } else {
579 options.outputDirectory = QFileInfo(options.outputDirectory).canonicalFilePath();
580 if (!options.outputDirectory.endsWith(c: u'/'))
581 options.outputDirectory += u'/';
582 }
583
584 return options;
585}
586
587void printHelp()
588{
589 fprintf(stderr, format: R"(
590Syntax: androiddeployqt --output <destination> [options]
591
592Creates an Android package in the build directory <destination> and
593builds it into an .apk file.
594
595Optional arguments:
596 --input <inputfile>: Reads <inputfile> for options generated by
597 qmake. A default file name based on the current working
598 directory will be used if nothing else is specified.
599
600 --deployment <mechanism>: Supported deployment mechanisms:
601 bundled (default): Includes Qt files in stand-alone package.
602 unbundled: Assumes native libraries are present on the device
603 and does not include them in the APK.
604
605 --aab: Build an Android App Bundle.
606
607 --no-build: Do not build the package, it is useful to just install
608 a package previously built.
609
610 --install: Installs apk to device/emulator. By default this step is
611 not taken. If the application has previously been installed on
612 the device, it will be uninstalled first.
613
614 --reinstall: Installs apk to device/emulator. By default this step
615 is not taken. If the application has previously been installed on
616 the device, it will be overwritten, but its data will be left
617 intact.
618
619 --device [device ID]: Use specified device for deployment. Default
620 is the device selected by default by adb.
621
622 --android-platform <platform>: Builds against the given android
623 platform. By default, the highest available version will be
624 used.
625
626 --release: Builds a package ready for release. By default, the
627 package will be signed with a debug key.
628
629 --sign <url/to/keystore> <alias>: Signs the package with the
630 specified keystore, alias and store password.
631 Optional arguments for use with signing:
632 --storepass <password>: Keystore password.
633 --storetype <type>: Keystore type.
634 --keypass <password>: Password for private key (if different
635 from keystore password.)
636 --sigfile <file>: Name of .SF/.DSA file.
637 --digestalg <name>: Name of digest algorithm. Default is
638 "SHA-256".
639 --sigalg <name>: Name of signature algorithm. Default is
640 "SHA256withRSA".
641 --tsa <url>: Location of the Time Stamping Authority.
642 --tsacert <alias>: Public key certificate for TSA.
643 --internalsf: Include the .SF file inside the signature block.
644 --sectionsonly: Do not compute hash of entire manifest.
645 --protected: Keystore has protected authentication path.
646 --jarsigner: Deprecated, ignored.
647
648 NOTE: To conceal the keystore information, the environment variables
649 QT_ANDROID_KEYSTORE_PATH, and QT_ANDROID_KEYSTORE_ALIAS are used to
650 set the values keysotore and alias respectively.
651 Also the environment variables QT_ANDROID_KEYSTORE_STORE_PASS,
652 and QT_ANDROID_KEYSTORE_KEY_PASS are used to set the store and key
653 passwords respectively. This option needs only the --sign parameter.
654
655 --jdk <path/to/jdk>: Used to find the jarsigner tool when used
656 in combination with the --release argument. By default,
657 an attempt is made to detect the tool using the JAVA_HOME and
658 PATH environment variables, in that order.
659
660 --qml-import-paths: Specify additional search paths for QML
661 imports.
662
663 --verbose: Prints out information during processing.
664
665 --no-generated-assets-cache: Do not pregenerate the entry list for
666 the assets file engine.
667
668 --aux-mode: Operate in auxiliary mode. This will only copy the
669 dependencies into the build directory and update the XML templates.
670 The project will not be built or installed.
671
672 --apk <path/where/to/copy/the/apk>: Path where to copy the built apk.
673
674 --build-aar: Build an AAR package. This option skips --aab, --install,
675 --reinstall, and --sign options if they are provided.
676
677 --qml-importscanner-binary <path/to/qmlimportscanner>: Override the
678 default qmlimportscanner binary path. By default the
679 qmlimportscanner binary is located using the Qt directory
680 specified in the input file.
681
682 --depfile <path/to/depfile>: Output a dependency file.
683
684 --builddir <path/to/build/directory>: build directory. Necessary when
685 generating a depfile because ninja requires relative paths.
686
687 --no-rcc-bundle-cleanup: skip cleaning rcc bundle directory after
688 running androiddeployqt. This option simplifies debugging of
689 the resource bundle content, but it should not be used when deploying
690 a project, since it litters the "assets" directory.
691
692 --copy-dependencies-only: resolve application dependencies and stop
693 deploying process after all libraries and resources that the
694 application depends on have been copied.
695
696 --help: Displays this information.
697)");
698}
699
700// Since strings compared will all start with the same letters,
701// sorting by length and then alphabetically within each length
702// gives the natural order.
703bool quasiLexicographicalReverseLessThan(const QFileInfo &fi1, const QFileInfo &fi2)
704{
705 QString s1 = fi1.baseName();
706 QString s2 = fi2.baseName();
707
708 if (s1.size() == s2.size())
709 return s1 > s2;
710 else
711 return s1.size() > s2.size();
712}
713
714// Files which contain templates that need to be overwritten by build data should be overwritten every
715// time.
716bool alwaysOverwritableFile(const QString &fileName)
717{
718 return (fileName.endsWith(s: "/res/values/libs.xml"_L1)
719 || fileName.endsWith(s: "/AndroidManifest.xml"_L1)
720 || fileName.endsWith(s: "/res/values/strings.xml"_L1)
721 || fileName.endsWith(s: "/src/org/qtproject/qt/android/bindings/QtActivity.java"_L1));
722}
723
724
725bool copyFileIfNewer(const QString &sourceFileName,
726 const QString &destinationFileName,
727 const Options &options,
728 bool forceOverwrite = false)
729{
730 dependenciesForDepfile << sourceFileName;
731 if (QFile::exists(fileName: destinationFileName)) {
732 QFileInfo destinationFileInfo(destinationFileName);
733 QFileInfo sourceFileInfo(sourceFileName);
734
735 if (!forceOverwrite
736 && sourceFileInfo.lastModified() <= destinationFileInfo.lastModified()
737 && !alwaysOverwritableFile(fileName: destinationFileName)) {
738 if (options.verbose)
739 fprintf(stdout, format: " -- Skipping file %s. Same or newer file already in place.\n", qPrintable(sourceFileName));
740 return true;
741 } else {
742 if (!QFile(destinationFileName).remove()) {
743 fprintf(stderr, format: "Can't remove old file: %s\n", qPrintable(destinationFileName));
744 return false;
745 }
746 }
747 }
748
749 if (!QDir().mkpath(dirPath: QFileInfo(destinationFileName).path())) {
750 fprintf(stderr, format: "Cannot make output directory for %s.\n", qPrintable(destinationFileName));
751 return false;
752 }
753
754 if (!QFile::exists(fileName: destinationFileName) && !QFile::copy(fileName: sourceFileName, newName: destinationFileName)) {
755 fprintf(stderr, format: "Failed to copy %s to %s.\n", qPrintable(sourceFileName), qPrintable(destinationFileName));
756 return false;
757 } else if (options.verbose) {
758 fprintf(stdout, format: " -- Copied %s\n", qPrintable(destinationFileName));
759 fflush(stdout);
760 }
761 return true;
762}
763
764struct GradleBuildConfigs {
765 QString appNamespace;
766 bool setsLegacyPackaging = false;
767 bool usesIntegerCompileSdkVersion = false;
768};
769
770GradleBuildConfigs gradleBuildConfigs(const QString &path)
771{
772 GradleBuildConfigs configs;
773
774 QFile file(path);
775 if (!file.open(flags: QIODevice::ReadOnly))
776 return configs;
777
778 auto isComment = [](const QByteArray &trimmed) {
779 return trimmed.startsWith(bv: "//") || trimmed.startsWith(c: '*') || trimmed.startsWith(bv: "/*");
780 };
781
782 auto extractValue = [](const QByteArray &trimmed) {
783 int idx = trimmed.indexOf(ch: '=');
784
785 if (idx == -1)
786 idx = trimmed.indexOf(ch: ' ');
787
788 if (idx > -1)
789 return trimmed.mid(index: idx + 1).trimmed();
790
791 return QByteArray();
792 };
793
794 const auto lines = file.readAll().split(sep: '\n');
795 for (const auto &line : lines) {
796 const QByteArray trimmedLine = line.trimmed();
797 if (isComment(trimmedLine))
798 continue;
799 if (trimmedLine.contains(bv: "useLegacyPackaging")) {
800 configs.setsLegacyPackaging = true;
801 } else if (trimmedLine.contains(bv: "compileSdkVersion androidCompileSdkVersion.toInteger()")) {
802 configs.usesIntegerCompileSdkVersion = true;
803 } else if (trimmedLine.contains(bv: "namespace")) {
804 const QString value = QString::fromUtf8(ba: extractValue(trimmedLine));
805 const bool singleQuoted = value.startsWith(c: u'\'') && value.endsWith(c: u'\'');
806 const bool doubleQuoted = value.startsWith(c: u'\"') && value.endsWith(c: u'\"');
807
808 if (singleQuoted || doubleQuoted)
809 configs.appNamespace = value.mid(position: 1, n: value.length() - 2);
810 else
811 configs.appNamespace = value;
812 }
813 }
814
815 return configs;
816}
817
818QString cleanPackageName(QString packageName, bool *cleaned = nullptr)
819{
820 auto isLegalChar = [] (QChar c) -> bool {
821 ushort ch = c.unicode();
822 return (ch >= '0' && ch <= '9') ||
823 (ch >= 'A' && ch <= 'Z') ||
824 (ch >= 'a' && ch <= 'z') ||
825 ch == '.' || ch == '_';
826 };
827
828 if (cleaned)
829 *cleaned = false;
830
831 for (QChar &c : packageName) {
832 if (!isLegalChar(c)) {
833 c = u'_';
834 if (cleaned)
835 *cleaned = true;
836 }
837 }
838
839 static QStringList keywords;
840 if (keywords.isEmpty()) {
841 keywords << "abstract"_L1 << "continue"_L1 << "for"_L1
842 << "new"_L1 << "switch"_L1 << "assert"_L1
843 << "default"_L1 << "if"_L1 << "package"_L1
844 << "synchronized"_L1 << "boolean"_L1 << "do"_L1
845 << "goto"_L1 << "private"_L1 << "this"_L1
846 << "break"_L1 << "double"_L1 << "implements"_L1
847 << "protected"_L1 << "throw"_L1 << "byte"_L1
848 << "else"_L1 << "import"_L1 << "public"_L1
849 << "throws"_L1 << "case"_L1 << "enum"_L1
850 << "instanceof"_L1 << "return"_L1 << "transient"_L1
851 << "catch"_L1 << "extends"_L1 << "int"_L1
852 << "short"_L1 << "try"_L1 << "char"_L1
853 << "final"_L1 << "interface"_L1 << "static"_L1
854 << "void"_L1 << "class"_L1 << "finally"_L1
855 << "long"_L1 << "strictfp"_L1 << "volatile"_L1
856 << "const"_L1 << "float"_L1 << "native"_L1
857 << "super"_L1 << "while"_L1;
858 }
859
860 // No keywords
861 qsizetype index = -1;
862 while (index < packageName.size()) {
863 qsizetype next = packageName.indexOf(ch: u'.', from: index + 1);
864 if (next == -1)
865 next = packageName.size();
866 QString word = packageName.mid(position: index + 1, n: next - index - 1);
867 if (!word.isEmpty()) {
868 QChar c = word[0];
869 if ((c >= u'0' && c <= u'9') || c == u'_') {
870 packageName.insert(i: index + 1, c: u'a');
871 if (cleaned)
872 *cleaned = true;
873 index = next + 1;
874 continue;
875 }
876 }
877 if (keywords.contains(str: word)) {
878 packageName.insert(i: next, s: "_"_L1);
879 if (cleaned)
880 *cleaned = true;
881 index = next + 1;
882 } else {
883 index = next;
884 }
885 }
886
887 return packageName;
888}
889
890QString detectLatestAndroidPlatform(const QString &sdkPath)
891{
892 QDir dir(sdkPath + "/platforms"_L1);
893 if (!dir.exists()) {
894 fprintf(stderr, format: "Directory %s does not exist\n", qPrintable(dir.absolutePath()));
895 return QString();
896 }
897
898 QFileInfoList fileInfos = dir.entryInfoList(filters: QDir::Dirs | QDir::NoDotAndDotDot);
899 if (fileInfos.isEmpty()) {
900 fprintf(stderr, format: "No platforms found in %s", qPrintable(dir.absolutePath()));
901 return QString();
902 }
903
904 std::sort(first: fileInfos.begin(), last: fileInfos.end(), comp: quasiLexicographicalReverseLessThan);
905
906 const QFileInfo& latestPlatform = fileInfos.constFirst();
907 return latestPlatform.baseName();
908}
909
910QString extractPackageName(Options *options)
911{
912 {
913 const QString gradleBuildFile = options->androidSourceDirectory + "/build.gradle"_L1;
914 QString packageName = gradleBuildConfigs(path: gradleBuildFile).appNamespace;
915
916 if (!packageName.isEmpty() && packageName != "androidPackageName"_L1)
917 return packageName;
918 }
919
920 QFile androidManifestXml(options->androidSourceDirectory + "/AndroidManifest.xml"_L1);
921 if (androidManifestXml.open(flags: QIODevice::ReadOnly)) {
922 QXmlStreamReader reader(&androidManifestXml);
923 while (!reader.atEnd()) {
924 reader.readNext();
925 if (reader.isStartElement() && reader.name() == "manifest"_L1) {
926 QString packageName = reader.attributes().value(qualifiedName: "package"_L1).toString();
927 if (!packageName.isEmpty() && packageName != "org.qtproject.example"_L1)
928 return packageName;
929 break;
930 }
931 }
932 }
933
934 return QString();
935}
936
937bool parseCmakeBoolean(const QJsonValue &value)
938{
939 const QString stringValue = value.toString();
940 return (stringValue.compare(s: QString::fromUtf8(utf8: "true"), cs: Qt::CaseInsensitive)
941 || stringValue.compare(s: QString::fromUtf8(utf8: "on"), cs: Qt::CaseInsensitive)
942 || stringValue.compare(s: QString::fromUtf8(utf8: "yes"), cs: Qt::CaseInsensitive)
943 || stringValue.compare(s: QString::fromUtf8(utf8: "y"), cs: Qt::CaseInsensitive)
944 || stringValue.toInt() > 0);
945}
946
947bool readInputFileDirectory(Options *options, QJsonObject &jsonObject, const QString keyName)
948{
949 const QJsonValue qtDirectory = jsonObject.value(key: keyName);
950 if (qtDirectory.isUndefined()) {
951 for (auto it = options->architectures.constBegin(); it != options->architectures.constEnd(); ++it) {
952 if (keyName == "qtDataDirectory"_L1) {
953 options->architectures[it.key()].qtDirectories[keyName] = "."_L1;
954 break;
955 } else if (keyName == "qtLibsDirectory"_L1) {
956 options->architectures[it.key()].qtDirectories[keyName] = "lib"_L1;
957 break;
958 } else if (keyName == "qtLibExecsDirectory"_L1) {
959 options->architectures[it.key()].qtDirectories[keyName] = defaultLibexecDir();
960 break;
961 } else if (keyName == "qtPluginsDirectory"_L1) {
962 options->architectures[it.key()].qtDirectories[keyName] = "plugins"_L1;
963 break;
964 } else if (keyName == "qtQmlDirectory"_L1) {
965 options->architectures[it.key()].qtDirectories[keyName] = "qml"_L1;
966 break;
967 }
968 }
969 return true;
970 }
971
972 if (qtDirectory.isObject()) {
973 const QJsonObject object = qtDirectory.toObject();
974 for (auto it = object.constBegin(); it != object.constEnd(); ++it) {
975 if (it.value().isUndefined()) {
976 fprintf(stderr,
977 format: "Invalid '%s' record in deployment settings: %s\n",
978 qPrintable(keyName),
979 qPrintable(it.value().toString()));
980 return false;
981 }
982 if (it.value().isNull())
983 continue;
984 if (!options->architectures.contains(key: it.key())) {
985 fprintf(stderr, format: "Architecture %s unknown (%s).", qPrintable(it.key()),
986 qPrintable(options->architectures.keys().join(u',')));
987 return false;
988 }
989 options->architectures[it.key()].qtDirectories[keyName] = it.value().toString();
990 }
991 } else if (qtDirectory.isString()) {
992 // Format for Qt < 6 or when using the tool with Qt >= 6 but in single arch.
993 // We assume Qt > 5.14 where all architectures are in the same directory.
994 const QString directory = qtDirectory.toString();
995 options->architectures["arm64-v8a"_L1].qtDirectories[keyName] = directory;
996 options->architectures["armeabi-v7a"_L1].qtDirectories[keyName] = directory;
997 options->architectures["x86"_L1].qtDirectories[keyName] = directory;
998 options->architectures["x86_64"_L1].qtDirectories[keyName] = directory;
999 } else {
1000 fprintf(stderr, format: "Invalid format for %s in json file %s.\n",
1001 qPrintable(keyName), qPrintable(options->inputFileName));
1002 return false;
1003 }
1004 return true;
1005}
1006
1007bool readInputFile(Options *options)
1008{
1009 QFile file(options->inputFileName);
1010 if (!file.open(flags: QIODevice::ReadOnly)) {
1011 fprintf(stderr, format: "Cannot read from input file: %s\n", qPrintable(options->inputFileName));
1012 return false;
1013 }
1014 dependenciesForDepfile << options->inputFileName;
1015
1016 QJsonDocument jsonDocument = QJsonDocument::fromJson(json: file.readAll());
1017 if (jsonDocument.isNull()) {
1018 fprintf(stderr, format: "Invalid json file: %s\n", qPrintable(options->inputFileName));
1019 return false;
1020 }
1021
1022 QJsonObject jsonObject = jsonDocument.object();
1023
1024 {
1025 QJsonValue sdkPath = jsonObject.value(key: "sdk"_L1);
1026 if (sdkPath.isUndefined()) {
1027 fprintf(stderr, format: "No SDK path in json file %s\n", qPrintable(options->inputFileName));
1028 return false;
1029 }
1030
1031 options->sdkPath = QDir::fromNativeSeparators(pathName: sdkPath.toString());
1032
1033 if (options->androidPlatform.isEmpty()) {
1034 options->androidPlatform = detectLatestAndroidPlatform(sdkPath: options->sdkPath);
1035 if (options->androidPlatform.isEmpty())
1036 return false;
1037 } else {
1038 if (!QDir(options->sdkPath + "/platforms/"_L1 + options->androidPlatform).exists()) {
1039 fprintf(stderr, format: "Warning: Android platform '%s' does not exist in SDK.\n",
1040 qPrintable(options->androidPlatform));
1041 }
1042 }
1043 }
1044
1045 {
1046
1047 const QJsonValue value = jsonObject.value(key: "sdkBuildToolsRevision"_L1);
1048 if (!value.isUndefined())
1049 options->sdkBuildToolsVersion = value.toString();
1050 }
1051
1052 {
1053 const QJsonValue qtInstallDirectory = jsonObject.value(key: "qt"_L1);
1054 if (qtInstallDirectory.isUndefined()) {
1055 fprintf(stderr, format: "No Qt directory in json file %s\n", qPrintable(options->inputFileName));
1056 return false;
1057 }
1058
1059 if (qtInstallDirectory.isObject()) {
1060 const QJsonObject object = qtInstallDirectory.toObject();
1061 for (auto it = object.constBegin(); it != object.constEnd(); ++it) {
1062 if (it.value().isUndefined()) {
1063 fprintf(stderr,
1064 format: "Invalid 'qt' record in deployment settings: %s\n",
1065 qPrintable(it.value().toString()));
1066 return false;
1067 }
1068 if (it.value().isNull())
1069 continue;
1070 options->architectures.insert(key: it.key(),
1071 value: QtInstallDirectoryWithTriple(it.value().toString()));
1072 }
1073 } else if (qtInstallDirectory.isString()) {
1074 // Format for Qt < 6 or when using the tool with Qt >= 6 but in single arch.
1075 // We assume Qt > 5.14 where all architectures are in the same directory.
1076 const QString directory = qtInstallDirectory.toString();
1077 QtInstallDirectoryWithTriple qtInstallDirectoryWithTriple(directory);
1078 options->architectures.insert(key: "arm64-v8a"_L1, value: qtInstallDirectoryWithTriple);
1079 options->architectures.insert(key: "armeabi-v7a"_L1, value: qtInstallDirectoryWithTriple);
1080 options->architectures.insert(key: "x86"_L1, value: qtInstallDirectoryWithTriple);
1081 options->architectures.insert(key: "x86_64"_L1, value: qtInstallDirectoryWithTriple);
1082 // In Qt < 6 rcc and qmlimportscanner are installed in the host and install directories
1083 // In Qt >= 6 rcc and qmlimportscanner are only installed in the host directory
1084 // So setting the "qtHostDir" is not necessary with Qt < 6.
1085 options->qtHostDirectory = directory;
1086 } else {
1087 fprintf(stderr, format: "Invalid format for Qt install prefixes in json file %s.\n",
1088 qPrintable(options->inputFileName));
1089 return false;
1090 }
1091 }
1092
1093 if (!readInputFileDirectory(options, jsonObject, keyName: "qtDataDirectory"_L1) ||
1094 !readInputFileDirectory(options, jsonObject, keyName: "qtLibsDirectory"_L1) ||
1095 !readInputFileDirectory(options, jsonObject, keyName: "qtLibExecsDirectory"_L1) ||
1096 !readInputFileDirectory(options, jsonObject, keyName: "qtPluginsDirectory"_L1) ||
1097 !readInputFileDirectory(options, jsonObject, keyName: "qtQmlDirectory"_L1))
1098 return false;
1099
1100 {
1101 const QJsonValue qtHostDirectory = jsonObject.value(key: "qtHostDir"_L1);
1102 if (!qtHostDirectory.isUndefined()) {
1103 if (qtHostDirectory.isString()) {
1104 options->qtHostDirectory = qtHostDirectory.toString();
1105 } else {
1106 fprintf(stderr, format: "Invalid format for Qt host directory in json file %s.\n",
1107 qPrintable(options->inputFileName));
1108 return false;
1109 }
1110 }
1111 }
1112
1113 {
1114 const auto extraPrefixDirs = jsonObject.value(key: "extraPrefixDirs"_L1).toArray();
1115 options->extraPrefixDirs.reserve(n: extraPrefixDirs.size());
1116 for (const QJsonValue prefix : extraPrefixDirs) {
1117 options->extraPrefixDirs.push_back(x: prefix.toString());
1118 }
1119 }
1120
1121 {
1122 const auto androidDeployPlugins = jsonObject.value(key: "android-deploy-plugins"_L1).toString();
1123 options->androidDeployPlugins = androidDeployPlugins.split(sep: ";"_L1, behavior: Qt::SkipEmptyParts);
1124 }
1125
1126 {
1127 const auto extraLibraryDirs = jsonObject.value(key: "extraLibraryDirs"_L1).toArray();
1128 options->extraLibraryDirs.reserve(n: extraLibraryDirs.size());
1129 for (const QJsonValue path : extraLibraryDirs) {
1130 options->extraLibraryDirs.push_back(x: path.toString());
1131 }
1132 }
1133
1134 {
1135 const QJsonValue androidSourcesDirectory = jsonObject.value(key: "android-package-source-directory"_L1);
1136 if (!androidSourcesDirectory.isUndefined())
1137 options->androidSourceDirectory = androidSourcesDirectory.toString();
1138 }
1139
1140 {
1141 const QJsonValue applicationArguments = jsonObject.value(key: "android-application-arguments"_L1);
1142 if (!applicationArguments.isUndefined())
1143 options->applicationArguments = applicationArguments.toString();
1144 else
1145 options->applicationArguments = QStringLiteral("");
1146 }
1147
1148 {
1149 const QJsonValue androidVersionName = jsonObject.value(key: "android-version-name"_L1);
1150 if (!androidVersionName.isUndefined())
1151 options->versionName = androidVersionName.toString();
1152 else
1153 options->versionName = QStringLiteral("1.0");
1154 }
1155
1156 {
1157 const QJsonValue androidVersionCode = jsonObject.value(key: "android-version-code"_L1);
1158 if (!androidVersionCode.isUndefined())
1159 options->versionCode = androidVersionCode.toString();
1160 else
1161 options->versionCode = QStringLiteral("1");
1162 }
1163
1164 {
1165 const QJsonValue ver = jsonObject.value(key: "android-min-sdk-version"_L1);
1166 if (!ver.isUndefined())
1167 options->minSdkVersion = ver.toString().toUtf8();
1168 }
1169
1170 {
1171 const QJsonValue ver = jsonObject.value(key: "android-target-sdk-version"_L1);
1172 if (!ver.isUndefined())
1173 options->targetSdkVersion = ver.toString().toUtf8();
1174 }
1175
1176 {
1177 const QJsonObject targetArchitectures = jsonObject.value(key: "architectures"_L1).toObject();
1178 if (targetArchitectures.isEmpty()) {
1179 fprintf(stderr, format: "No target architecture defined in json file.\n");
1180 return false;
1181 }
1182 for (auto it = targetArchitectures.constBegin(); it != targetArchitectures.constEnd(); ++it) {
1183 if (it.value().isUndefined()) {
1184 fprintf(stderr, format: "Invalid architecture.\n");
1185 return false;
1186 }
1187 if (it.value().isNull())
1188 continue;
1189 if (!options->architectures.contains(key: it.key())) {
1190 fprintf(stderr, format: "Architecture %s unknown (%s).", qPrintable(it.key()),
1191 qPrintable(options->architectures.keys().join(u',')));
1192 return false;
1193 }
1194 options->architectures[it.key()].triple = it.value().toString();
1195 options->architectures[it.key()].enabled = true;
1196 }
1197 }
1198
1199 {
1200 const QJsonValue ndk = jsonObject.value(key: "ndk"_L1);
1201 if (ndk.isUndefined()) {
1202 fprintf(stderr, format: "No NDK path defined in json file.\n");
1203 return false;
1204 }
1205 options->ndkPath = ndk.toString();
1206 const QString ndkPropertiesPath = options->ndkPath + QStringLiteral("/source.properties");
1207 const QSettings settings(ndkPropertiesPath, QSettings::IniFormat);
1208 const QString ndkVersion = settings.value(QStringLiteral("Pkg.Revision")).toString();
1209 if (ndkVersion.isEmpty()) {
1210 fprintf(stderr, format: "Couldn't retrieve the NDK version from \"%s\".\n",
1211 qPrintable(ndkPropertiesPath));
1212 return false;
1213 }
1214 options->ndkVersion = ndkVersion;
1215 }
1216
1217 {
1218 const QJsonValue toolchainPrefix = jsonObject.value(key: "toolchain-prefix"_L1);
1219 if (toolchainPrefix.isUndefined()) {
1220 fprintf(stderr, format: "No toolchain prefix defined in json file.\n");
1221 return false;
1222 }
1223 options->toolchainPrefix = toolchainPrefix.toString();
1224 }
1225
1226 {
1227 const QJsonValue ndkHost = jsonObject.value(key: "ndk-host"_L1);
1228 if (ndkHost.isUndefined()) {
1229 fprintf(stderr, format: "No NDK host defined in json file.\n");
1230 return false;
1231 }
1232 options->ndkHost = ndkHost.toString();
1233 }
1234
1235 {
1236 const QJsonValue extraLibs = jsonObject.value(key: "android-extra-libs"_L1);
1237 if (!extraLibs.isUndefined())
1238 options->extraLibs = extraLibs.toString().split(sep: u',', behavior: Qt::SkipEmptyParts);
1239 }
1240
1241 {
1242 const QJsonValue qmlSkipImportScanning = jsonObject.value(key: "qml-skip-import-scanning"_L1);
1243 if (!qmlSkipImportScanning.isUndefined())
1244 options->qmlSkipImportScanning = qmlSkipImportScanning.toBool();
1245 }
1246
1247 {
1248 const QJsonValue extraPlugins = jsonObject.value(key: "android-extra-plugins"_L1);
1249 if (!extraPlugins.isUndefined())
1250 options->extraPlugins = extraPlugins.toString().split(sep: u',');
1251 }
1252
1253 {
1254 const QJsonValue systemLibsPath =
1255 jsonObject.value(key: "android-system-libs-prefix"_L1);
1256 if (!systemLibsPath.isUndefined())
1257 options->systemLibsPath = systemLibsPath.toString();
1258 }
1259
1260 {
1261 const QJsonValue noDeploy = jsonObject.value(key: "android-no-deploy-qt-libs"_L1);
1262 if (!noDeploy.isUndefined()) {
1263 bool useUnbundled = parseCmakeBoolean(value: noDeploy);
1264 options->deploymentMechanism = useUnbundled ? Options::Unbundled :
1265 Options::Bundled;
1266 }
1267 }
1268
1269 {
1270 const QJsonValue stdcppPath = jsonObject.value(key: "stdcpp-path"_L1);
1271 if (stdcppPath.isUndefined()) {
1272 fprintf(stderr, format: "No stdcpp-path defined in json file.\n");
1273 return false;
1274 }
1275 options->stdCppPath = stdcppPath.toString();
1276 }
1277
1278 {
1279 const QJsonValue qmlRootPath = jsonObject.value(key: "qml-root-path"_L1);
1280 if (qmlRootPath.isString()) {
1281 options->rootPaths.push_back(x: qmlRootPath.toString());
1282 } else if (qmlRootPath.isArray()) {
1283 auto qmlRootPaths = qmlRootPath.toArray();
1284 for (auto path : qmlRootPaths) {
1285 if (path.isString())
1286 options->rootPaths.push_back(x: path.toString());
1287 }
1288 } else {
1289 options->rootPaths.push_back(x: QFileInfo(options->inputFileName).absolutePath());
1290 }
1291 }
1292
1293 {
1294 const QJsonValue qmlImportPaths = jsonObject.value(key: "qml-import-paths"_L1);
1295 if (!qmlImportPaths.isUndefined())
1296 options->qmlImportPaths = qmlImportPaths.toString().split(sep: u',');
1297 }
1298
1299 {
1300 const QJsonValue qmlImportScannerBinaryPath = jsonObject.value(key: "qml-importscanner-binary"_L1);
1301 if (!qmlImportScannerBinaryPath.isUndefined())
1302 options->qmlImportScannerBinaryPath = qmlImportScannerBinaryPath.toString();
1303 }
1304
1305 {
1306 const QJsonValue rccBinaryPath = jsonObject.value(key: "rcc-binary"_L1);
1307 if (!rccBinaryPath.isUndefined())
1308 options->rccBinaryPath = rccBinaryPath.toString();
1309 }
1310
1311 {
1312 const QJsonValue genJavaQmlComponents = jsonObject.value(key: "generate-java-qtquickview-contents"_L1);
1313 if (!genJavaQmlComponents.isUndefined() && genJavaQmlComponents.isBool()) {
1314 options->generateJavaQmlComponents = genJavaQmlComponents.toBool(defaultValue: false);
1315 if (options->generateJavaQmlComponents && !options->buildAar) {
1316 fprintf(stderr,
1317 format: "Warning: Skipping the generation of Java QtQuickView contents from QML "
1318 "as it can be enabled only for an AAR target.\n");
1319 options->generateJavaQmlComponents = false;
1320 }
1321 }
1322 }
1323
1324 {
1325 const QJsonValue qmlDomBinaryPath = jsonObject.value(key: "qml-dom-binary"_L1);
1326 if (!qmlDomBinaryPath.isUndefined()) {
1327 options->qmlDomBinaryPath = qmlDomBinaryPath.toString();
1328 } else if (options->generateJavaQmlComponents) {
1329 fprintf(stderr,
1330 format: "No qmldom binary defined in json file which is required when "
1331 "building with QT_ANDROID_GENERATE_JAVA_QTQUICKVIEW_CONTENTS flag.\n");
1332 return false;
1333 }
1334 }
1335
1336 {
1337 const QJsonValue qmlFiles = jsonObject.value(key: "qml-files-for-code-generator"_L1);
1338 if (!qmlFiles.isUndefined() && qmlFiles.isArray()) {
1339 const QJsonArray jArray = qmlFiles.toArray();
1340 for (auto &item : jArray)
1341 options->selectedJavaQmlComponents << item.toString();
1342 }
1343 }
1344
1345 {
1346 const QJsonValue applicationBinary = jsonObject.value(key: "application-binary"_L1);
1347 if (applicationBinary.isUndefined()) {
1348 fprintf(stderr, format: "No application binary defined in json file.\n");
1349 return false;
1350 }
1351 options->applicationBinary = applicationBinary.toString();
1352 if (options->build) {
1353 for (auto it = options->architectures.constBegin(); it != options->architectures.constEnd(); ++it) {
1354 if (!it->enabled)
1355 continue;
1356 auto appBinaryPath = "%1/libs/%2/lib%3_%2.so"_L1.arg(args&: options->outputDirectory, args: it.key(), args&: options->applicationBinary);
1357 if (!QFile::exists(fileName: appBinaryPath)) {
1358 fprintf(stderr, format: "Cannot find application binary in build dir %s.\n", qPrintable(appBinaryPath));
1359 return false;
1360 }
1361 }
1362 }
1363 }
1364
1365 {
1366 const QJsonValue androidPackageName = jsonObject.value(key: "android-package-name"_L1);
1367 const QString extractedPackageName = extractPackageName(options);
1368 if (!extractedPackageName.isEmpty())
1369 options->packageName = extractedPackageName;
1370 else if (!androidPackageName.isUndefined())
1371 options->packageName = androidPackageName.toString();
1372 else
1373 options->packageName = "org.qtproject.example.%1"_L1.arg(args&: options->applicationBinary);
1374
1375 bool cleaned;
1376 options->packageName = cleanPackageName(packageName: options->packageName, cleaned: &cleaned);
1377 if (cleaned) {
1378 fprintf(stderr, format: "Warning: Package name contained illegal characters and was cleaned "
1379 "to \"%s\"\n", qPrintable(options->packageName));
1380 }
1381 }
1382
1383 {
1384 using ItFlag = QDirListing::IteratorFlag;
1385 const QJsonValue deploymentDependencies = jsonObject.value(key: "deployment-dependencies"_L1);
1386 if (!deploymentDependencies.isUndefined()) {
1387 QString deploymentDependenciesString = deploymentDependencies.toString();
1388 const auto dependencies = QStringView{deploymentDependenciesString}.split(sep: u',');
1389 for (const auto &dependency : dependencies) {
1390 QString path = options->qtInstallDirectory + QChar::fromLatin1(c: '/');
1391 path += dependency;
1392 if (QFileInfo(path).isDir()) {
1393 for (const auto &dirEntry : QDirListing(path, ItFlag::Recursive)) {
1394 if (dirEntry.isFile()) {
1395 const QString subPath = dirEntry.filePath();
1396 auto arch = fileArchitecture(options: *options, path: subPath);
1397 if (!arch.isEmpty()) {
1398 options->qtDependencies[arch].append(t: QtDependency(subPath.mid(position: options->qtInstallDirectory.size() + 1),
1399 subPath));
1400 } else if (options->verbose) {
1401 fprintf(stderr, format: "Skipping \"%s\", unknown architecture\n", qPrintable(subPath));
1402 fflush(stderr);
1403 }
1404 }
1405 }
1406 } else {
1407 auto qtDependency = [options](const QStringView &dependency,
1408 const QString &arch) {
1409 const auto installDir = options->architectures[arch].qtInstallDirectory;
1410 const auto absolutePath = "%1/%2"_L1.arg(args: installDir, args: dependency.toString());
1411 return QtDependency(dependency.toString(), absolutePath);
1412 };
1413
1414 if (dependency.endsWith(s: QLatin1String(".so"))) {
1415 auto arch = fileArchitecture(options: *options, path);
1416 if (!arch.isEmpty()) {
1417 options->qtDependencies[arch].append(t: qtDependency(dependency, arch));
1418 } else if (options->verbose) {
1419 fprintf(stderr, format: "Skipping \"%s\", unknown architecture\n", qPrintable(path));
1420 fflush(stderr);
1421 }
1422 } else {
1423 for (auto arch : options->architectures.keys())
1424 options->qtDependencies[arch].append(t: qtDependency(dependency, arch));
1425 }
1426 }
1427 }
1428 }
1429 }
1430 {
1431 const QJsonValue qrcFiles = jsonObject.value(key: "qrcFiles"_L1);
1432 options->qrcFiles = qrcFiles.toString().split(sep: u',', behavior: Qt::SkipEmptyParts);
1433 }
1434 {
1435 const QJsonValue zstdCompressionFlag = jsonObject.value(key: "zstdCompression"_L1);
1436 if (zstdCompressionFlag.isBool()) {
1437 options->isZstdCompressionEnabled = zstdCompressionFlag.toBool();
1438 }
1439 }
1440
1441 return true;
1442}
1443
1444bool isDeployment(const Options *options, Options::DeploymentMechanism deployment)
1445{
1446 return options->deploymentMechanism == deployment;
1447}
1448
1449bool copyFiles(const QDir &sourceDirectory, const QDir &destinationDirectory, const Options &options, bool forceOverwrite = false)
1450{
1451 const QFileInfoList entries = sourceDirectory.entryInfoList(filters: QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs);
1452 for (const QFileInfo &entry : entries) {
1453 if (entry.isDir()) {
1454 QDir dir(entry.absoluteFilePath());
1455 if (!destinationDirectory.mkpath(dirPath: dir.dirName())) {
1456 fprintf(stderr, format: "Cannot make directory %s in %s\n", qPrintable(dir.dirName()), qPrintable(destinationDirectory.path()));
1457 return false;
1458 }
1459
1460 if (!copyFiles(sourceDirectory: dir, destinationDirectory: QDir(destinationDirectory.path() + u'/' + dir.dirName()), options, forceOverwrite))
1461 return false;
1462 } else {
1463 QString destination = destinationDirectory.absoluteFilePath(fileName: entry.fileName());
1464 if (!copyFileIfNewer(sourceFileName: entry.absoluteFilePath(), destinationFileName: destination, options, forceOverwrite))
1465 return false;
1466 }
1467 }
1468
1469 return true;
1470}
1471
1472void cleanTopFolders(const Options &options, const QDir &srcDir, const QString &dstDir)
1473{
1474 const auto dirs = srcDir.entryInfoList(filters: QDir::NoDotAndDotDot | QDir::Dirs);
1475 for (const QFileInfo &dir : dirs) {
1476 if (dir.fileName() != "libs"_L1)
1477 deleteMissingFiles(options, srcDir: dir.absoluteFilePath(), dstDir: QDir(dstDir + dir.fileName()));
1478 }
1479}
1480
1481void cleanAndroidFiles(const Options &options)
1482{
1483 if (!options.androidSourceDirectory.isEmpty())
1484 cleanTopFolders(options, srcDir: QDir(options.androidSourceDirectory), dstDir: options.outputDirectory);
1485
1486 cleanTopFolders(options,
1487 srcDir: QDir(options.qtInstallDirectory + u'/' +
1488 options.qtDataDirectory + "/src/android/templates"_L1),
1489 dstDir: options.outputDirectory);
1490}
1491
1492bool copyAndroidTemplate(const Options &options, const QString &androidTemplate, const QString &outDirPrefix = QString())
1493{
1494 QDir sourceDirectory(options.qtInstallDirectory + u'/' + options.qtDataDirectory + androidTemplate);
1495 if (!sourceDirectory.exists()) {
1496 fprintf(stderr, format: "Cannot find template directory %s\n", qPrintable(sourceDirectory.absolutePath()));
1497 return false;
1498 }
1499
1500 QString outDir = options.outputDirectory + outDirPrefix;
1501
1502 if (!QDir::current().mkpath(dirPath: outDir)) {
1503 fprintf(stderr, format: "Cannot create output directory %s\n", qPrintable(options.outputDirectory));
1504 return false;
1505 }
1506
1507 return copyFiles(sourceDirectory, destinationDirectory: QDir(outDir), options);
1508}
1509
1510bool copyGradleTemplate(const Options &options)
1511{
1512 QDir sourceDirectory(options.qtInstallDirectory + u'/' +
1513 options.qtDataDirectory + "/src/3rdparty/gradle"_L1);
1514 if (!sourceDirectory.exists()) {
1515 fprintf(stderr, format: "Cannot find template directory %s\n", qPrintable(sourceDirectory.absolutePath()));
1516 return false;
1517 }
1518
1519 QString outDir(options.outputDirectory);
1520 if (!QDir::current().mkpath(dirPath: outDir)) {
1521 fprintf(stderr, format: "Cannot create output directory %s\n", qPrintable(options.outputDirectory));
1522 return false;
1523 }
1524
1525 return copyFiles(sourceDirectory, destinationDirectory: QDir(outDir), options);
1526}
1527
1528bool copyAndroidTemplate(const Options &options)
1529{
1530 if (options.verbose)
1531 fprintf(stdout, format: "Copying Android package template.\n");
1532
1533 if (!copyGradleTemplate(options))
1534 return false;
1535
1536 if (!copyAndroidTemplate(options, androidTemplate: "/src/android/templates"_L1))
1537 return false;
1538
1539 if (options.buildAar)
1540 return copyAndroidTemplate(options, androidTemplate: "/src/android/templates_aar"_L1);
1541
1542 return true;
1543}
1544
1545bool copyAndroidSources(const Options &options)
1546{
1547 if (options.androidSourceDirectory.isEmpty())
1548 return true;
1549
1550 if (options.verbose)
1551 fprintf(stdout, format: "Copying Android sources from project.\n");
1552
1553 QDir sourceDirectory(options.androidSourceDirectory);
1554 if (!sourceDirectory.exists()) {
1555 fprintf(stderr, format: "Cannot find android sources in %s", qPrintable(options.androidSourceDirectory));
1556 return false;
1557 }
1558
1559 return copyFiles(sourceDirectory, destinationDirectory: QDir(options.outputDirectory), options, forceOverwrite: true);
1560}
1561
1562bool copyAndroidExtraLibs(Options *options)
1563{
1564 if (options->extraLibs.isEmpty())
1565 return true;
1566
1567 if (options->verbose) {
1568 switch (options->deploymentMechanism) {
1569 case Options::Bundled:
1570 fprintf(stdout, format: "Copying %zd external libraries to package.\n", size_t(options->extraLibs.size()));
1571 break;
1572 case Options::Unbundled:
1573 fprintf(stdout, format: "Skip copying of external libraries.\n");
1574 break;
1575 };
1576 }
1577
1578 for (const QString &extraLib : options->extraLibs) {
1579 QFileInfo extraLibInfo(extraLib);
1580 if (!extraLibInfo.exists()) {
1581 fprintf(stderr, format: "External library %s does not exist!\n", qPrintable(extraLib));
1582 return false;
1583 }
1584 if (!checkArchitecture(options: *options, fileName: extraLibInfo.filePath())) {
1585 if (options->verbose)
1586 fprintf(stdout, format: "Skipping \"%s\", architecture mismatch.\n", qPrintable(extraLib));
1587 continue;
1588 }
1589 if (!extraLibInfo.fileName().startsWith(s: "lib"_L1) || extraLibInfo.suffix() != "so"_L1) {
1590 fprintf(stderr, format: "The file name of external library %s must begin with \"lib\" and end with the suffix \".so\".\n",
1591 qPrintable(extraLib));
1592 return false;
1593 }
1594 QString destinationFile(options->outputDirectory
1595 + "/libs/"_L1
1596 + options->currentArchitecture
1597 + u'/'
1598 + extraLibInfo.fileName());
1599
1600 if (isDeployment(options, deployment: Options::Bundled)
1601 && !copyFileIfNewer(sourceFileName: extraLib, destinationFileName: destinationFile, options: *options)) {
1602 return false;
1603 }
1604 options->archExtraLibs[options->currentArchitecture] += extraLib;
1605 }
1606
1607 return true;
1608}
1609
1610QStringList allFilesInside(const QDir& current, const QDir& rootDir)
1611{
1612 QStringList result;
1613 const auto dirs = current.entryList(filters: QDir::Dirs|QDir::NoDotAndDotDot);
1614 const auto files = current.entryList(filters: QDir::Files);
1615 result.reserve(asize: dirs.size() + files.size());
1616 for (const QString &dir : dirs) {
1617 result += allFilesInside(current: QDir(current.filePath(fileName: dir)), rootDir);
1618 }
1619 for (const QString &file : files) {
1620 result += rootDir.relativeFilePath(fileName: current.filePath(fileName: file));
1621 }
1622 return result;
1623}
1624
1625bool copyAndroidExtraResources(Options *options)
1626{
1627 if (options->extraPlugins.isEmpty())
1628 return true;
1629
1630 if (options->verbose)
1631 fprintf(stdout, format: "Copying %zd external resources to package.\n", size_t(options->extraPlugins.size()));
1632
1633 for (const QString &extraResource : options->extraPlugins) {
1634 QFileInfo extraResourceInfo(extraResource);
1635 if (!extraResourceInfo.exists() || !extraResourceInfo.isDir()) {
1636 fprintf(stderr, format: "External resource %s does not exist or not a correct directory!\n", qPrintable(extraResource));
1637 return false;
1638 }
1639
1640 QDir resourceDir(extraResource);
1641 QString assetsDir = options->outputDirectory + "/assets/"_L1 +
1642 resourceDir.dirName() + u'/';
1643 QString libsDir = options->outputDirectory + "/libs/"_L1 + options->currentArchitecture + u'/';
1644
1645 const QStringList files = allFilesInside(current: resourceDir, rootDir: resourceDir);
1646 for (const QString &resourceFile : files) {
1647 QString originFile(resourceDir.filePath(fileName: resourceFile));
1648 QString destinationFile;
1649 if (!resourceFile.endsWith(s: ".so"_L1)) {
1650 destinationFile = assetsDir + resourceFile;
1651 } else {
1652 if (isDeployment(options, deployment: Options::Unbundled)
1653 || !checkArchitecture(options: *options, fileName: originFile)) {
1654 continue;
1655 }
1656 destinationFile = libsDir + resourceFile;
1657 options->archExtraPlugins[options->currentArchitecture] += resourceFile;
1658 }
1659 if (!copyFileIfNewer(sourceFileName: originFile, destinationFileName: destinationFile, options: *options))
1660 return false;
1661 }
1662 }
1663
1664 return true;
1665}
1666
1667bool updateFile(const QString &fileName, const QHash<QString, QString> &replacements)
1668{
1669 QFile inputFile(fileName);
1670 if (!inputFile.open(flags: QIODevice::ReadOnly)) {
1671 fprintf(stderr, format: "Cannot open %s for reading.\n", qPrintable(fileName));
1672 return false;
1673 }
1674
1675 // All the files we are doing substitutes in are quite small. If this
1676 // ever changes, this code should be updated to be more conservative.
1677 QByteArray contents = inputFile.readAll();
1678
1679 bool hasReplacements = false;
1680 QHash<QString, QString>::const_iterator it;
1681 for (it = replacements.constBegin(); it != replacements.constEnd(); ++it) {
1682 if (it.key() == it.value())
1683 continue; // Nothing to actually replace
1684
1685 forever {
1686 int index = contents.indexOf(bv: it.key().toUtf8());
1687 if (index >= 0) {
1688 contents.replace(index, len: it.key().size(), s: it.value().toUtf8());
1689 hasReplacements = true;
1690 } else {
1691 break;
1692 }
1693 }
1694 }
1695
1696 if (hasReplacements) {
1697 inputFile.close();
1698
1699 if (!inputFile.open(flags: QIODevice::WriteOnly)) {
1700 fprintf(stderr, format: "Cannot open %s for writing.\n", qPrintable(fileName));
1701 return false;
1702 }
1703
1704 inputFile.write(data: contents);
1705 }
1706
1707 return true;
1708
1709}
1710
1711bool updateLibsXml(Options *options)
1712{
1713 if (options->verbose)
1714 fprintf(stdout, format: " -- res/values/libs.xml\n");
1715
1716 QString fileName = options->outputDirectory + "/res/values/libs.xml"_L1;
1717 if (!QFile::exists(fileName)) {
1718 fprintf(stderr, format: "Cannot find %s in prepared packaged. This file is required.\n", qPrintable(fileName));
1719 return false;
1720 }
1721
1722 QString qtLibs;
1723 QString allLocalLibs;
1724 QString extraLibs;
1725
1726 for (auto it = options->architectures.constBegin(); it != options->architectures.constEnd(); ++it) {
1727 if (!it->enabled)
1728 continue;
1729
1730 qtLibs += " <item>%1;%2</item>\n"_L1.arg(args: it.key(), args&: options->stdCppName);
1731 for (const Options::BundledFile &bundledFile : options->bundledFiles[it.key()]) {
1732 if (bundledFile.second.startsWith(s: "lib/lib"_L1)) {
1733 if (!bundledFile.second.endsWith(s: ".so"_L1)) {
1734 fprintf(stderr,
1735 format: "The bundled library %s doesn't end with .so. Android only supports "
1736 "versionless libraries ending with the .so suffix.\n",
1737 qPrintable(bundledFile.second));
1738 return false;
1739 }
1740 QString s = bundledFile.second.mid(position: sizeof("lib/lib") - 1);
1741 s.chop(n: sizeof(".so") - 1);
1742 qtLibs += " <item>%1;%2</item>\n"_L1.arg(args: it.key(), args&: s);
1743 }
1744 }
1745
1746 if (!options->archExtraLibs[it.key()].isEmpty()) {
1747 for (const QString &extraLib : options->archExtraLibs[it.key()]) {
1748 QFileInfo extraLibInfo(extraLib);
1749 if (extraLibInfo.fileName().startsWith(s: "lib"_L1)) {
1750 if (!extraLibInfo.fileName().endsWith(s: ".so"_L1)) {
1751 fprintf(stderr,
1752 format: "The library %s doesn't end with .so. Android only supports "
1753 "versionless libraries ending with the .so suffix.\n",
1754 qPrintable(extraLibInfo.fileName()));
1755 return false;
1756 }
1757 QString name = extraLibInfo.fileName().mid(position: sizeof("lib") - 1);
1758 name.chop(n: sizeof(".so") - 1);
1759 extraLibs += " <item>%1;%2</item>\n"_L1.arg(args: it.key(), args&: name);
1760 }
1761 }
1762 }
1763
1764 QStringList localLibs;
1765 localLibs = options->localLibs[it.key()];
1766 const QString archSuffix = it.key() + ".so"_L1;
1767
1768 const QList<QtDependency>& deps = options->qtDependencies[it.key()];
1769 auto notExistsInDependencies = [&deps, archSuffix] (const QString &libName) {
1770 QString lib = QFileInfo(libName).fileName();
1771 if (lib.endsWith(s: archSuffix))
1772 lib.chop(n: archSuffix.length());
1773 return std::none_of(first: deps.begin(), last: deps.end(), pred: [&lib] (const QtDependency &dep) {
1774 return QFileInfo(dep.absolutePath).fileName().contains(s: lib);
1775 });
1776 };
1777
1778 // Clean up localLibs: remove libs that were not added to qtDependecies
1779 localLibs.erase(abegin: std::remove_if(first: localLibs.begin(), last: localLibs.end(), pred: notExistsInDependencies),
1780 aend: localLibs.end());
1781
1782 // If .pro file overrides dependency detection, we need to see which platform plugin they picked
1783 if (localLibs.isEmpty()) {
1784 QString plugin;
1785 for (const QtDependency &qtDependency : deps) {
1786 if (qtDependency.relativePath.contains(s: "libplugins_platforms_qtforandroid_"_L1))
1787 plugin = qtDependency.relativePath;
1788
1789 if (qtDependency.relativePath.contains(
1790 s: QString::asprintf(format: "libQt%dOpenGL", QT_VERSION_MAJOR))
1791 || qtDependency.relativePath.contains(
1792 s: QString::asprintf(format: "libQt%dQuick", QT_VERSION_MAJOR))) {
1793 options->usesOpenGL |= true;
1794 }
1795 }
1796
1797 if (plugin.isEmpty()) {
1798 fflush(stdout);
1799 fprintf(stderr, format: "No platform plugin (libplugins_platforms_qtforandroid.so) included"
1800 " in the deployment. Make sure the app links to Qt Gui library.\n");
1801 fflush(stderr);
1802 return false;
1803 }
1804
1805 localLibs.append(t: plugin);
1806 if (options->verbose)
1807 fprintf(stdout, format: " -- Using platform plugin %s\n", qPrintable(plugin));
1808 }
1809
1810 // remove all paths
1811 for (auto &lib : localLibs) {
1812 if (lib.endsWith(s: ".so"_L1))
1813 lib = lib.mid(position: lib.lastIndexOf(c: u'/') + 1);
1814 }
1815 allLocalLibs += " <item>%1;%2</item>\n"_L1.arg(args: it.key(), args: localLibs.join(sep: u':'));
1816 }
1817
1818 options->initClasses.removeDuplicates();
1819
1820 QHash<QString, QString> replacements;
1821 replacements[QStringLiteral("<!-- %%INSERT_QT_LIBS%% -->")] += qtLibs.trimmed();
1822 replacements[QStringLiteral("<!-- %%INSERT_LOCAL_LIBS%% -->")] = allLocalLibs.trimmed();
1823 replacements[QStringLiteral("<!-- %%INSERT_EXTRA_LIBS%% -->")] = extraLibs.trimmed();
1824 const QString initClasses = options->initClasses.join(sep: u':');
1825 replacements[QStringLiteral("<!-- %%INSERT_INIT_CLASSES%% -->")] = initClasses;
1826
1827 // Set BUNDLE_LOCAL_QT_LIBS based on the deployment used
1828 replacements[QStringLiteral("<!-- %%BUNDLE_LOCAL_QT_LIBS%% -->")]
1829 = isDeployment(options, deployment: Options::Unbundled) ? "0"_L1 : "1"_L1;
1830 replacements[QStringLiteral("<!-- %%USE_LOCAL_QT_LIBS%% -->")] = "1"_L1;
1831 replacements[QStringLiteral("<!-- %%SYSTEM_LIBS_PREFIX%% -->")] =
1832 isDeployment(options, deployment: Options::Unbundled) ? options->systemLibsPath : QStringLiteral("");
1833
1834 if (!updateFile(fileName, replacements))
1835 return false;
1836
1837 return true;
1838}
1839
1840bool updateStringsXml(const Options &options)
1841{
1842 if (options.verbose)
1843 fprintf(stdout, format: " -- res/values/strings.xml\n");
1844
1845 QHash<QString, QString> replacements;
1846 replacements[QStringLiteral("<!-- %%INSERT_APP_NAME%% -->")] = options.applicationBinary;
1847
1848 QString fileName = options.outputDirectory + "/res/values/strings.xml"_L1;
1849 if (!QFile::exists(fileName)) {
1850 if (options.verbose)
1851 fprintf(stdout, format: " -- Create strings.xml since it's missing.\n");
1852 QFile file(fileName);
1853 if (!file.open(flags: QIODevice::WriteOnly)) {
1854 fprintf(stderr, format: "Can't open %s for writing.\n", qPrintable(fileName));
1855 return false;
1856 }
1857 file.write(data: QByteArray("<?xml version='1.0' encoding='utf-8'?><resources><string name=\"app_name\" translatable=\"false\">")
1858 .append(a: options.applicationBinary.toLatin1())
1859 .append(s: "</string></resources>\n"));
1860 return true;
1861 }
1862
1863 if (!updateFile(fileName, replacements))
1864 return false;
1865
1866 return true;
1867}
1868
1869bool updateAndroidManifest(Options &options)
1870{
1871 if (options.verbose)
1872 fprintf(stdout, format: " -- AndroidManifest.xml \n");
1873
1874 QHash<QString, QString> replacements;
1875 replacements[QStringLiteral("-- %%INSERT_APP_NAME%% --")] = options.applicationBinary;
1876 replacements[QStringLiteral("-- %%INSERT_APP_ARGUMENTS%% --")] = options.applicationArguments;
1877 replacements[QStringLiteral("-- %%INSERT_APP_LIB_NAME%% --")] = options.applicationBinary;
1878 replacements[QStringLiteral("-- %%INSERT_VERSION_NAME%% --")] = options.versionName;
1879 replacements[QStringLiteral("-- %%INSERT_VERSION_CODE%% --")] = options.versionCode;
1880 replacements[QStringLiteral("package=\"org.qtproject.example\"")] = "package=\"%1\""_L1.arg(args&: options.packageName);
1881
1882 const QString androidManifestPath = options.outputDirectory + "/AndroidManifest.xml"_L1;
1883 QFile androidManifestXml(androidManifestPath);
1884 // User may have manually defined permissions in the AndroidManifest.xml
1885 // Read these permissions in order to remove any duplicates, as otherwise the
1886 // application build would fail.
1887 if (androidManifestXml.exists() && androidManifestXml.open(flags: QIODevice::ReadOnly)) {
1888 QXmlStreamReader reader(&androidManifestXml);
1889 while (!reader.atEnd()) {
1890 reader.readNext();
1891 if (reader.isStartElement() && reader.name() == "uses-permission"_L1)
1892 options.permissions.remove(key: QString(reader.attributes().value(qualifiedName: "android:name"_L1)));
1893 }
1894 androidManifestXml.close();
1895 }
1896
1897 QString permissions;
1898 for (auto [name, extras] : options.permissions.asKeyValueRange())
1899 permissions += " <uses-permission android:name=\"%1\" %2 />\n"_L1.arg(args: name).arg(a: extras);
1900 replacements[QStringLiteral("<!-- %%INSERT_PERMISSIONS -->")] = permissions.trimmed();
1901
1902 QString features;
1903 for (const QString &feature : std::as_const(t&: options.features))
1904 features += " <uses-feature android:name=\"%1\" android:required=\"false\" />\n"_L1.arg(args: feature);
1905 if (options.usesOpenGL)
1906 features += " <uses-feature android:glEsVersion=\"0x00020000\" android:required=\"true\" />"_L1;
1907
1908 replacements[QStringLiteral("<!-- %%INSERT_FEATURES -->")] = features.trimmed();
1909
1910 if (!updateFile(fileName: androidManifestPath, replacements))
1911 return false;
1912
1913 // read the package, min & target sdk API levels from manifest file.
1914 bool checkOldAndroidLabelString = false;
1915 if (androidManifestXml.exists()) {
1916 if (!androidManifestXml.open(flags: QIODevice::ReadOnly)) {
1917 fprintf(stderr, format: "Cannot open %s for reading.\n", qPrintable(androidManifestPath));
1918 return false;
1919 }
1920
1921 QXmlStreamReader reader(&androidManifestXml);
1922 while (!reader.atEnd()) {
1923 reader.readNext();
1924
1925 if (reader.isStartElement()) {
1926 if (reader.name() == "uses-sdk"_L1) {
1927 if (reader.attributes().hasAttribute(qualifiedName: "android:minSdkVersion"_L1))
1928 if (reader.attributes().value(qualifiedName: "android:minSdkVersion"_L1).toInt() < 28) {
1929 fprintf(stderr, format: "Invalid minSdkVersion version, minSdkVersion must be >= 28\n");
1930 return false;
1931 }
1932 } else if ((reader.name() == "application"_L1 ||
1933 reader.name() == "activity"_L1) &&
1934 reader.attributes().hasAttribute(qualifiedName: "android:label"_L1) &&
1935 reader.attributes().value(qualifiedName: "android:label"_L1) == "@string/app_name"_L1) {
1936 checkOldAndroidLabelString = true;
1937 } else if (reader.name() == "meta-data"_L1) {
1938 const auto name = reader.attributes().value(qualifiedName: "android:name"_L1);
1939 const auto value = reader.attributes().value(qualifiedName: "android:value"_L1);
1940 if (name == "android.app.lib_name"_L1 && value.contains(c: u' ')) {
1941 fprintf(stderr, format: "The Activity's android.app.lib_name should not contain"
1942 " spaces.\n");
1943 return false;
1944 }
1945 }
1946 }
1947 }
1948
1949 if (reader.hasError()) {
1950 fprintf(stderr, format: "Error in %s: %s\n", qPrintable(androidManifestPath), qPrintable(reader.errorString()));
1951 return false;
1952 }
1953 } else {
1954 fprintf(stderr, format: "No android manifest file");
1955 return false;
1956 }
1957
1958 if (checkOldAndroidLabelString)
1959 updateStringsXml(options);
1960
1961 return true;
1962}
1963
1964bool updateAndroidFiles(Options &options)
1965{
1966 if (options.verbose)
1967 fprintf(stdout, format: "Updating Android package files with project settings.\n");
1968
1969 if (!updateLibsXml(options: &options))
1970 return false;
1971
1972 if (!updateAndroidManifest(options))
1973 return false;
1974
1975 return true;
1976}
1977
1978static QString absoluteFilePath(const Options *options, const QString &relativeFileName)
1979{
1980 // Use extraLibraryDirs as the extra library lookup folder if it is expected to find a file in
1981 // any $prefix/lib folder.
1982 // Library directories from a build tree(extraLibraryDirs) have the higher priority.
1983 if (relativeFileName.startsWith(s: "lib/"_L1)) {
1984 for (const auto &dir : options->extraLibraryDirs) {
1985 const QString path = dir + u'/' + relativeFileName.mid(position: sizeof("lib/") - 1);
1986 if (QFile::exists(fileName: path))
1987 return path;
1988 }
1989 }
1990
1991 for (const auto &prefix : options->extraPrefixDirs) {
1992 const QString path = prefix + u'/' + relativeFileName;
1993 if (QFile::exists(fileName: path))
1994 return path;
1995 }
1996
1997 if (relativeFileName.endsWith(s: "-android-dependencies.xml"_L1)) {
1998 for (const auto &dir : options->extraLibraryDirs) {
1999 const QString path = dir + u'/' + relativeFileName;
2000 if (QFile::exists(fileName: path))
2001 return path;
2002 }
2003 return options->qtInstallDirectory + u'/' + options->qtLibsDirectory +
2004 u'/' + relativeFileName;
2005 }
2006
2007 if (relativeFileName.startsWith(s: "jar/"_L1)) {
2008 return options->qtInstallDirectory + u'/' + options->qtDataDirectory +
2009 u'/' + relativeFileName;
2010 }
2011
2012 if (relativeFileName.startsWith(s: "lib/"_L1)) {
2013 return options->qtInstallDirectory + u'/' + options->qtLibsDirectory +
2014 u'/' + relativeFileName.mid(position: sizeof("lib/") - 1);
2015 }
2016 return options->qtInstallDirectory + u'/' + relativeFileName;
2017}
2018
2019QList<QtDependency> findFilesRecursively(const Options &options, const QFileInfo &info, const QString &rootPath)
2020{
2021 if (!info.exists())
2022 return QList<QtDependency>();
2023
2024 if (info.isDir()) {
2025 QList<QtDependency> ret;
2026
2027 QDir dir(info.filePath());
2028 const QStringList entries = dir.entryList(filters: QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
2029
2030 for (const QString &entry : entries) {
2031 ret += findFilesRecursively(options,
2032 info: QFileInfo(info.absoluteFilePath() + QChar(u'/') + entry),
2033 rootPath);
2034 }
2035
2036 return ret;
2037 } else {
2038 return QList<QtDependency>() << QtDependency(info.absoluteFilePath().mid(position: rootPath.size()), info.absoluteFilePath());
2039 }
2040}
2041
2042QList<QtDependency> findFilesRecursively(const Options &options, const QString &fileName)
2043{
2044 // We try to find the fileName in extraPrefixDirs first. The function behaves differently
2045 // depending on what the fileName points to. If fileName is a file then we try to find the
2046 // first occurrence in extraPrefixDirs and return this file. If fileName is directory function
2047 // iterates over it and looks for deployment artifacts in each 'extraPrefixDirs' entry.
2048 // Also we assume that if the fileName is recognized as a directory once it will be directory
2049 // for every 'extraPrefixDirs' entry.
2050 QList<QtDependency> deps;
2051 for (const auto &prefix : options.extraPrefixDirs) {
2052 QFileInfo info(prefix + u'/' + fileName);
2053 if (info.exists()) {
2054 if (info.isDir())
2055 deps.append(other: findFilesRecursively(options, info, rootPath: prefix + u'/'));
2056 else
2057 return findFilesRecursively(options, info, rootPath: prefix + u'/');
2058 }
2059 }
2060
2061 // Usually android deployment settings contain Qt install directory in extraPrefixDirs.
2062 if (std::find(first: options.extraPrefixDirs.begin(), last: options.extraPrefixDirs.end(),
2063 val: options.qtInstallDirectory) == options.extraPrefixDirs.end()) {
2064 QFileInfo info(options.qtInstallDirectory + "/"_L1 + fileName);
2065 QFileInfo rootPath(options.qtInstallDirectory + "/"_L1);
2066 deps.append(other: findFilesRecursively(options, info, rootPath: rootPath.absolutePath()));
2067 }
2068 return deps;
2069}
2070
2071void readDependenciesFromFiles(Options *options, const QList<QtDependency> &files,
2072 QSet<QString> &usedDependencies,
2073 QSet<QString> &remainingDependencies)
2074{
2075 for (const QtDependency &fileName : files) {
2076 if (usedDependencies.contains(value: fileName.absolutePath))
2077 continue;
2078
2079 if (fileName.absolutePath.endsWith(s: ".so"_L1)) {
2080 if (!readDependenciesFromElf(options, fileName: fileName.absolutePath, usedDependencies: &usedDependencies,
2081 remainingDependencies: &remainingDependencies)) {
2082 fprintf(stdout, format: "Skipping file dependency: %s\n",
2083 qPrintable(fileName.relativePath));
2084 continue;
2085 }
2086 }
2087 usedDependencies.insert(value: fileName.absolutePath);
2088
2089 if (options->verbose) {
2090 fprintf(stdout, format: "Appending file dependency: %s\n", qPrintable(fileName.relativePath));
2091 }
2092
2093 options->qtDependencies[options->currentArchitecture].append(t: fileName);
2094 }
2095}
2096
2097bool readAndroidDependencyXml(Options *options,
2098 const QString &moduleName,
2099 QSet<QString> *usedDependencies,
2100 QSet<QString> *remainingDependencies)
2101{
2102 QString androidDependencyName = absoluteFilePath(options, relativeFileName: "%1-android-dependencies.xml"_L1.arg(args: moduleName));
2103
2104 QFile androidDependencyFile(androidDependencyName);
2105 if (androidDependencyFile.exists()) {
2106 if (options->verbose)
2107 fprintf(stdout, format: "Reading Android dependencies for %s\n", qPrintable(moduleName));
2108
2109 if (!androidDependencyFile.open(flags: QIODevice::ReadOnly)) {
2110 fprintf(stderr, format: "Cannot open %s for reading.\n", qPrintable(androidDependencyName));
2111 return false;
2112 }
2113
2114 QXmlStreamReader reader(&androidDependencyFile);
2115 while (!reader.atEnd()) {
2116 reader.readNext();
2117
2118 if (reader.isStartElement()) {
2119 if (reader.name() == "bundled"_L1) {
2120 if (!reader.attributes().hasAttribute(qualifiedName: "file"_L1)) {
2121 fprintf(stderr, format: "Invalid android dependency file: %s\n", qPrintable(androidDependencyName));
2122 return false;
2123 }
2124
2125 QString file = reader.attributes().value(qualifiedName: "file"_L1).toString();
2126
2127 if (reader.attributes().hasAttribute(qualifiedName: "type"_L1)
2128 && reader.attributes().value(qualifiedName: "type"_L1) == "plugin_dir"_L1
2129 && !options->androidDeployPlugins.isEmpty()) {
2130 continue;
2131 }
2132
2133 const QList<QtDependency> fileNames = findFilesRecursively(options: *options, fileName: file);
2134 readDependenciesFromFiles(options, files: fileNames, usedDependencies&: *usedDependencies,
2135 remainingDependencies&: *remainingDependencies);
2136 } else if (reader.name() == "jar"_L1) {
2137 int bundling = reader.attributes().value(qualifiedName: "bundling"_L1).toInt();
2138 QString fileName = QDir::cleanPath(path: reader.attributes().value(qualifiedName: "file"_L1).toString());
2139 if (bundling) {
2140 QtDependency dependency(fileName, absoluteFilePath(options, relativeFileName: fileName));
2141 if (!usedDependencies->contains(value: dependency.absolutePath)) {
2142 options->qtDependencies[options->currentArchitecture].append(t: dependency);
2143 usedDependencies->insert(value: dependency.absolutePath);
2144 }
2145 }
2146
2147 if (reader.attributes().hasAttribute(qualifiedName: "initClass"_L1)) {
2148 options->initClasses.append(t: reader.attributes().value(qualifiedName: "initClass"_L1).toString());
2149 }
2150 } else if (reader.name() == "lib"_L1) {
2151 QString fileName = QDir::cleanPath(path: reader.attributes().value(qualifiedName: "file"_L1).toString());
2152 if (reader.attributes().hasAttribute(qualifiedName: "replaces"_L1)) {
2153 QString replaces = reader.attributes().value(qualifiedName: "replaces"_L1).toString();
2154 for (int i=0; i<options->localLibs.size(); ++i) {
2155 if (options->localLibs[options->currentArchitecture].at(i) == replaces) {
2156 options->localLibs[options->currentArchitecture][i] = fileName;
2157 break;
2158 }
2159 }
2160 } else if (!fileName.isEmpty()) {
2161 options->localLibs[options->currentArchitecture].append(t: fileName);
2162 }
2163 if (fileName.endsWith(s: ".so"_L1) && checkArchitecture(options: *options, fileName)) {
2164 remainingDependencies->insert(value: fileName);
2165 }
2166 } else if (reader.name() == "permission"_L1) {
2167 QString name = reader.attributes().value(qualifiedName: "name"_L1).toString();
2168 QString extras = reader.attributes().value(qualifiedName: "extras"_L1).toString();
2169 // With duplicate permissions prioritize the one without any attributes,
2170 // as that is likely the most permissive
2171 if (!options->permissions.contains(key: name)
2172 || !options->permissions.value(key: name).isEmpty()) {
2173 options->permissions.insert(key: name, value: extras);
2174 }
2175 } else if (reader.name() == "feature"_L1) {
2176 QString name = reader.attributes().value(qualifiedName: "name"_L1).toString();
2177 options->features.append(t: name);
2178 }
2179 }
2180 }
2181
2182 if (reader.hasError()) {
2183 fprintf(stderr, format: "Error in %s: %s\n", qPrintable(androidDependencyName), qPrintable(reader.errorString()));
2184 return false;
2185 }
2186 } else if (options->verbose) {
2187 fprintf(stdout, format: "No android dependencies for %s\n", qPrintable(moduleName));
2188 }
2189 options->features.removeDuplicates();
2190
2191 return true;
2192}
2193
2194QStringList getQtLibsFromElf(const Options &options, const QString &fileName)
2195{
2196 QString readElf = llvmReadobjPath(options);
2197 if (!QFile::exists(fileName: readElf)) {
2198 fprintf(stderr, format: "Command does not exist: %s\n", qPrintable(readElf));
2199 return QStringList();
2200 }
2201
2202 readElf = "%1 --needed-libs %2"_L1.arg(args: shellQuote(arg: readElf), args: shellQuote(arg: fileName));
2203
2204 auto readElfCommand = openProcess(command: readElf);
2205 if (!readElfCommand) {
2206 fprintf(stderr, format: "Cannot execute command %s\n", qPrintable(readElf));
2207 return QStringList();
2208 }
2209
2210 QStringList ret;
2211
2212 bool readLibs = false;
2213 char buffer[512];
2214 while (fgets(s: buffer, n: sizeof(buffer), stream: readElfCommand.get()) != nullptr) {
2215 QByteArray line = QByteArray::fromRawData(data: buffer, size: qstrlen(str: buffer));
2216 QString library;
2217 line = line.trimmed();
2218 if (!readLibs) {
2219 if (line.startsWith(bv: "Arch: ")) {
2220 auto it = elfArchitectures.find(key: line.mid(index: 6));
2221 if (it == elfArchitectures.constEnd() || *it != options.currentArchitecture.toLatin1()) {
2222 if (options.verbose)
2223 fprintf(stdout, format: "Skipping \"%s\", architecture mismatch\n", qPrintable(fileName));
2224 return {};
2225 }
2226 }
2227 readLibs = line.startsWith(bv: "NeededLibraries");
2228 continue;
2229 }
2230 if (!line.startsWith(bv: "lib"))
2231 continue;
2232 library = QString::fromLatin1(ba: line);
2233 QString libraryName = "lib/"_L1 + library;
2234 if (QFile::exists(fileName: absoluteFilePath(options: &options, relativeFileName: libraryName)))
2235 ret += libraryName;
2236 }
2237
2238 return ret;
2239}
2240
2241bool readDependenciesFromElf(Options *options,
2242 const QString &fileName,
2243 QSet<QString> *usedDependencies,
2244 QSet<QString> *remainingDependencies)
2245{
2246 // Get dependencies on libraries in $QTDIR/lib
2247 const QStringList dependencies = getQtLibsFromElf(options: *options, fileName);
2248
2249 if (options->verbose) {
2250 fprintf(stdout, format: "Reading dependencies from %s\n", qPrintable(fileName));
2251 for (const QString &dep : dependencies)
2252 fprintf(stdout, format: " %s\n", qPrintable(dep));
2253 }
2254 // Recursively add dependencies from ELF and supplementary XML information
2255 QList<QString> dependenciesToCheck;
2256 for (const QString &dependency : dependencies) {
2257 if (usedDependencies->contains(value: dependency))
2258 continue;
2259
2260 QString absoluteDependencyPath = absoluteFilePath(options, relativeFileName: dependency);
2261 usedDependencies->insert(value: dependency);
2262 if (!readDependenciesFromElf(options,
2263 fileName: absoluteDependencyPath,
2264 usedDependencies,
2265 remainingDependencies)) {
2266 return false;
2267 }
2268
2269 options->qtDependencies[options->currentArchitecture].append(t: QtDependency(dependency, absoluteDependencyPath));
2270 if (options->verbose)
2271 fprintf(stdout, format: "Appending dependency: %s\n", qPrintable(dependency));
2272 dependenciesToCheck.append(t: dependency);
2273 }
2274
2275 for (const QString &dependency : std::as_const(t&: dependenciesToCheck)) {
2276 QString qtBaseName = dependency.mid(position: sizeof("lib/lib") - 1);
2277 qtBaseName = qtBaseName.left(n: qtBaseName.size() - (sizeof(".so") - 1));
2278 if (!readAndroidDependencyXml(options, moduleName: qtBaseName, usedDependencies, remainingDependencies)) {
2279 return false;
2280 }
2281 }
2282
2283 return true;
2284}
2285
2286bool scanImports(Options *options, QSet<QString> *usedDependencies)
2287{
2288 if (options->verbose)
2289 fprintf(stdout, format: "Scanning for QML imports.\n");
2290
2291 QString qmlImportScanner;
2292 if (!options->qmlImportScannerBinaryPath.isEmpty()) {
2293 qmlImportScanner = options->qmlImportScannerBinaryPath;
2294 } else {
2295 qmlImportScanner = execSuffixAppended(path: options->qtLibExecsDirectory +
2296 "/qmlimportscanner"_L1);
2297 }
2298
2299 QStringList importPaths;
2300
2301 // In Conan's case, qtInstallDirectory will point only to qtbase installed files, which
2302 // lacks a qml directory. We don't want to pass it as an import path if it doesn't exist
2303 // because it will cause qmlimportscanner to fail.
2304 // This also covers the case when only qtbase is installed in a regular Qt build.
2305 const QString mainImportPath = options->qtInstallDirectory + u'/' + options->qtQmlDirectory;
2306 if (QFile::exists(fileName: mainImportPath))
2307 importPaths += shellQuote(arg: mainImportPath);
2308
2309 // These are usually provided by CMake in the deployment json file from paths specified
2310 // in CMAKE_FIND_ROOT_PATH. They might not have qml modules.
2311 for (const QString &prefix : options->extraPrefixDirs)
2312 if (QFile::exists(fileName: prefix + "/qml"_L1))
2313 importPaths += shellQuote(arg: prefix + "/qml"_L1);
2314
2315 // These are provided by both CMake and qmake.
2316 for (const QString &qmlImportPath : std::as_const(t&: options->qmlImportPaths)) {
2317 if (QFile::exists(fileName: qmlImportPath)) {
2318 importPaths += shellQuote(arg: qmlImportPath);
2319 } else {
2320 fprintf(stderr, format: "Warning: QML import path %s does not exist.\n",
2321 qPrintable(qmlImportPath));
2322 }
2323 }
2324
2325 bool qmlImportExists = false;
2326
2327 for (const QString &import : importPaths) {
2328 if (QDir().exists(name: import)) {
2329 qmlImportExists = true;
2330 break;
2331 }
2332 }
2333
2334 // Check importPaths without rootPath, since we need at least one qml plugins
2335 // folder to run a QML file
2336 if (!qmlImportExists) {
2337 fprintf(stderr, format: "Warning: no 'qml' directory found under Qt install directory "
2338 "or import paths. Skipping QML dependency scanning.\n");
2339 return true;
2340 }
2341
2342 if (!QFile::exists(fileName: qmlImportScanner)) {
2343 fprintf(stderr, format: "%s: qmlimportscanner not found at %s\n",
2344 qmlImportExists ? "Error"_L1.data() : "Warning"_L1.data(),
2345 qPrintable(qmlImportScanner));
2346 return true;
2347 }
2348
2349 for (auto rootPath : options->rootPaths) {
2350 rootPath = QFileInfo(rootPath).absoluteFilePath();
2351
2352 if (!rootPath.endsWith(c: u'/'))
2353 rootPath += u'/';
2354
2355 // After checking for qml folder imports we can add rootPath
2356 if (!rootPath.isEmpty())
2357 importPaths += shellQuote(arg: rootPath);
2358
2359 qmlImportScanner += " -rootPath %1"_L1.arg(args: shellQuote(arg: rootPath));
2360 }
2361
2362 if (!options->qrcFiles.isEmpty()) {
2363 qmlImportScanner += " -qrcFiles"_L1;
2364 for (const QString &qrcFile : options->qrcFiles)
2365 qmlImportScanner += u' ' + shellQuote(arg: qrcFile);
2366 }
2367
2368 qmlImportScanner += " -importPath %1"_L1.arg(args: importPaths.join(sep: u' '));
2369
2370 if (options->verbose) {
2371 fprintf(stdout, format: "Running qmlimportscanner with the following command: %s\n",
2372 qmlImportScanner.toLocal8Bit().constData());
2373 }
2374
2375 auto qmlImportScannerCommand = openProcess(command: qmlImportScanner);
2376 if (qmlImportScannerCommand == 0) {
2377 fprintf(stderr, format: "Couldn't run qmlimportscanner.\n");
2378 return false;
2379 }
2380
2381 QByteArray output;
2382 char buffer[512];
2383 while (fgets(s: buffer, n: sizeof(buffer), stream: qmlImportScannerCommand.get()) != nullptr)
2384 output += QByteArray(buffer, qstrlen(str: buffer));
2385
2386 QJsonDocument jsonDocument = QJsonDocument::fromJson(json: output);
2387 if (jsonDocument.isNull()) {
2388 fprintf(stderr, format: "Invalid json output from qmlimportscanner.\n");
2389 return false;
2390 }
2391
2392 QJsonArray jsonArray = jsonDocument.array();
2393 for (int i=0; i<jsonArray.count(); ++i) {
2394 QJsonValue value = jsonArray.at(i);
2395 if (!value.isObject()) {
2396 fprintf(stderr, format: "Invalid format of qmlimportscanner output.\n");
2397 return false;
2398 }
2399
2400 QJsonObject object = value.toObject();
2401 QString path = object.value(key: "path"_L1).toString();
2402 if (path.isEmpty()) {
2403 fprintf(stderr, format: "Warning: QML import could not be resolved in any of the import paths: %s\n",
2404 qPrintable(object.value("name"_L1).toString()));
2405 } else if (object.value(key: "type"_L1).toString() == "module"_L1) {
2406 if (options->verbose)
2407 fprintf(stdout, format: " -- Adding '%s' as QML dependency\n", qPrintable(path));
2408
2409 QFileInfo info(path);
2410
2411 // The qmlimportscanner sometimes outputs paths that do not exist.
2412 if (!info.exists()) {
2413 if (options->verbose)
2414 fprintf(stdout, format: " -- Skipping because path does not exist.\n");
2415 continue;
2416 }
2417
2418 QString absolutePath = info.absolutePath();
2419 if (!absolutePath.endsWith(c: u'/'))
2420 absolutePath += u'/';
2421
2422 const QUrl url(object.value(key: "name"_L1).toString());
2423
2424 const QString moduleUrlPath = u"/"_s + url.toString().replace(before: u'.', after: u'/');
2425 if (checkCanImportFromRootPaths(options, absolutePath: info.absolutePath(), moduleUrl: moduleUrlPath)) {
2426 if (options->verbose)
2427 fprintf(stdout, format: " -- Skipping because path is in QML root path.\n");
2428 continue;
2429 }
2430
2431 QString importPathOfThisImport;
2432 for (const QString &importPath : std::as_const(t&: importPaths)) {
2433 QString cleanImportPath = QDir::cleanPath(path: importPath);
2434 if (QFile::exists(fileName: cleanImportPath + moduleUrlPath)) {
2435 importPathOfThisImport = importPath;
2436 break;
2437 }
2438 }
2439
2440 if (importPathOfThisImport.isEmpty()) {
2441 fprintf(stderr, format: "Import found outside of import paths: %s.\n", qPrintable(info.absoluteFilePath()));
2442 return false;
2443 }
2444
2445 importPathOfThisImport = QDir(importPathOfThisImport).absolutePath() + u'/';
2446 QList<QtDependency> qmlImportsDependencies;
2447 auto collectQmlDependency = [&usedDependencies, &qmlImportsDependencies,
2448 &importPathOfThisImport](const QString &filePath) {
2449 if (!usedDependencies->contains(value: filePath)) {
2450 usedDependencies->insert(value: filePath);
2451 qmlImportsDependencies += QtDependency(
2452 "qml/"_L1 + filePath.mid(position: importPathOfThisImport.size()),
2453 filePath);
2454 }
2455 };
2456
2457 QString plugin = object.value(key: "plugin"_L1).toString();
2458 bool pluginIsOptional = object.value(key: "pluginIsOptional"_L1).toBool();
2459 QFileInfo pluginFileInfo = QFileInfo(
2460 path + u'/' + "lib"_L1 + plugin + u'_'
2461 + options->currentArchitecture + ".so"_L1);
2462 QString pluginFilePath = pluginFileInfo.absoluteFilePath();
2463 QSet<QString> remainingDependencies;
2464 if (pluginFileInfo.exists() && checkArchitecture(options: *options, fileName: pluginFilePath)
2465 && readDependenciesFromElf(options, fileName: pluginFilePath, usedDependencies,
2466 remainingDependencies: &remainingDependencies)) {
2467 collectQmlDependency(pluginFilePath);
2468 } else if (!pluginIsOptional) {
2469 if (options->verbose)
2470 fprintf(stdout, format: " -- Skipping because the required plugin is missing.\n");
2471 continue;
2472 }
2473
2474 QFileInfo qmldirFileInfo = QFileInfo(path + u'/' + "qmldir"_L1);
2475 if (qmldirFileInfo.exists()) {
2476 collectQmlDependency(qmldirFileInfo.absoluteFilePath());
2477 }
2478
2479 QString prefer = object.value(key: "prefer"_L1).toString();
2480 // If the preferred location of Qml files points to the Qt resources, this means
2481 // that all Qml files has been embedded into plugin and we should not copy them to the
2482 // android rcc bundle
2483 if (!prefer.startsWith(s: ":/"_L1)) {
2484 QVariantList qmlFiles =
2485 object.value(key: "components"_L1).toArray().toVariantList();
2486 qmlFiles.append(other: object.value(key: "scripts"_L1).toArray().toVariantList());
2487 bool qmlFilesMissing = false;
2488 for (const auto &qmlFileEntry : qmlFiles) {
2489 QFileInfo fileInfo(qmlFileEntry.toString());
2490 if (!fileInfo.exists()) {
2491 qmlFilesMissing = true;
2492 break;
2493 }
2494 collectQmlDependency(fileInfo.absoluteFilePath());
2495 }
2496
2497 if (qmlFilesMissing) {
2498 if (options->verbose)
2499 fprintf(stdout,
2500 format: " -- Skipping because the required qml files are missing.\n");
2501 continue;
2502 }
2503 }
2504
2505 options->qtDependencies[options->currentArchitecture].append(l: qmlImportsDependencies);
2506 } else {
2507 // We don't need to handle file and directory imports. Generally those should be
2508 // considered as part of the application and are therefore scanned separately.
2509 }
2510 }
2511
2512 return true;
2513}
2514
2515bool checkCanImportFromRootPaths(const Options *options, const QString &absolutePath,
2516 const QString &moduleUrlPath)
2517{
2518 for (auto rootPath : options->rootPaths) {
2519 if ((rootPath + moduleUrlPath) == absolutePath)
2520 return true;
2521 }
2522 return false;
2523}
2524
2525bool runCommand(const Options &options, const QString &command)
2526{
2527 if (options.verbose)
2528 fprintf(stdout, format: "Running command '%s'\n", qPrintable(command));
2529
2530 auto runCommand = openProcess(command);
2531 if (runCommand == nullptr) {
2532 fprintf(stderr, format: "Cannot run command '%s'\n", qPrintable(command));
2533 return false;
2534 }
2535 char buffer[4096];
2536 while (fgets(s: buffer, n: sizeof(buffer), stream: runCommand.get()) != nullptr) {
2537 if (options.verbose)
2538 fprintf(stdout, format: "%s", buffer);
2539 }
2540 runCommand.reset();
2541 fflush(stdout);
2542 fflush(stderr);
2543 return true;
2544}
2545
2546bool createRcc(const Options &options)
2547{
2548 auto assetsDir = "%1/assets"_L1.arg(args: options.outputDirectory);
2549 if (!QDir{"%1/android_rcc_bundle"_L1.arg(args&: assetsDir)}.exists()) {
2550 fprintf(stdout, format: "Skipping createRCC\n");
2551 return true;
2552 }
2553
2554 if (options.verbose)
2555 fprintf(stdout, format: "Create rcc bundle.\n");
2556
2557
2558 QString rcc;
2559 if (!options.rccBinaryPath.isEmpty()) {
2560 rcc = options.rccBinaryPath;
2561 } else {
2562 rcc = execSuffixAppended(path: options.qtLibExecsDirectory + "/rcc"_L1);
2563 }
2564
2565 if (!QFile::exists(fileName: rcc)) {
2566 fprintf(stderr, format: "rcc not found: %s\n", qPrintable(rcc));
2567 return false;
2568 }
2569 auto currentDir = QDir::currentPath();
2570 if (!QDir::setCurrent("%1/android_rcc_bundle"_L1.arg(args&: assetsDir))) {
2571 fprintf(stderr, format: "Cannot set current dir to: %s\n", qPrintable("%1/android_rcc_bundle"_L1.arg(assetsDir)));
2572 return false;
2573 }
2574
2575 bool res = runCommand(options, command: "%1 --project -o %2"_L1.arg(args&: rcc, args: shellQuote(arg: "%1/android_rcc_bundle.qrc"_L1.arg(args&: assetsDir))));
2576 if (!res)
2577 return false;
2578
2579 QLatin1StringView noZstd;
2580 if (!options.isZstdCompressionEnabled)
2581 noZstd = "--no-zstd"_L1;
2582
2583 QFile::rename(oldName: "%1/android_rcc_bundle.qrc"_L1.arg(args&: assetsDir), newName: "%1/android_rcc_bundle/android_rcc_bundle.qrc"_L1.arg(args&: assetsDir));
2584
2585 res = runCommand(options, command: "%1 %2 %3 --binary -o %4 android_rcc_bundle.qrc"_L1.arg(args&: rcc, args: shellQuote(arg: "--root=/android_rcc_bundle/"_L1),
2586 args&: noZstd,
2587 args: shellQuote(arg: "%1/android_rcc_bundle.rcc"_L1.arg(args&: assetsDir))));
2588 if (!QDir::setCurrent(currentDir)) {
2589 fprintf(stderr, format: "Cannot set current dir to: %s\n", qPrintable(currentDir));
2590 return false;
2591 }
2592 if (!options.noRccBundleCleanup) {
2593 QFile::remove(fileName: "%1/android_rcc_bundle.qrc"_L1.arg(args&: assetsDir));
2594 QDir{"%1/android_rcc_bundle"_L1.arg(args&: assetsDir)}.removeRecursively();
2595 }
2596 return res;
2597}
2598
2599bool readDependencies(Options *options)
2600{
2601 if (options->verbose)
2602 fprintf(stdout, format: "Detecting dependencies of application.\n");
2603
2604 // Override set in .pro file
2605 if (!options->qtDependencies[options->currentArchitecture].isEmpty()) {
2606 if (options->verbose)
2607 fprintf(stdout, format: "\tDependencies explicitly overridden in .pro file. No detection needed.\n");
2608 return true;
2609 }
2610
2611 QSet<QString> usedDependencies;
2612 QSet<QString> remainingDependencies;
2613
2614 // Add dependencies of application binary first
2615 if (!readDependenciesFromElf(options, fileName: "%1/libs/%2/lib%3_%2.so"_L1.arg(args&: options->outputDirectory, args&: options->currentArchitecture, args&: options->applicationBinary), usedDependencies: &usedDependencies, remainingDependencies: &remainingDependencies))
2616 return false;
2617
2618 QList<QtDependency> pluginDeps;
2619 for (const auto &pluginPath : options->androidDeployPlugins) {
2620 pluginDeps.append(other: findFilesRecursively(options: *options, info: QFileInfo(pluginPath),
2621 rootPath: options->qtInstallDirectory + "/"_L1));
2622 }
2623
2624 readDependenciesFromFiles(options, files: pluginDeps, usedDependencies, remainingDependencies);
2625
2626 while (!remainingDependencies.isEmpty()) {
2627 QSet<QString>::iterator start = remainingDependencies.begin();
2628 QString fileName = absoluteFilePath(options, relativeFileName: *start);
2629 remainingDependencies.erase(i: start);
2630
2631 QStringList unmetDependencies;
2632 if (goodToCopy(options, file: fileName, unmetDependencies: &unmetDependencies)) {
2633 bool ok = readDependenciesFromElf(options, fileName, usedDependencies: &usedDependencies, remainingDependencies: &remainingDependencies);
2634 if (!ok)
2635 return false;
2636 } else {
2637 fprintf(stdout, format: "Skipping %s due to unmet dependencies: %s\n",
2638 qPrintable(fileName),
2639 qPrintable(unmetDependencies.join(u',')));
2640 }
2641 }
2642
2643 QStringList::iterator it = options->localLibs[options->currentArchitecture].begin();
2644 while (it != options->localLibs[options->currentArchitecture].end()) {
2645 QStringList unmetDependencies;
2646 if (!goodToCopy(options, file: absoluteFilePath(options, relativeFileName: *it), unmetDependencies: &unmetDependencies)) {
2647 fprintf(stdout, format: "Skipping %s due to unmet dependencies: %s\n",
2648 qPrintable(*it),
2649 qPrintable(unmetDependencies.join(u',')));
2650 it = options->localLibs[options->currentArchitecture].erase(pos: it);
2651 } else {
2652 ++it;
2653 }
2654 }
2655
2656 if (options->qmlSkipImportScanning
2657 || (options->rootPaths.empty() && options->qrcFiles.isEmpty()))
2658 return true;
2659 return scanImports(options, usedDependencies: &usedDependencies);
2660}
2661
2662bool containsApplicationBinary(Options *options)
2663{
2664 if (!options->build)
2665 return true;
2666
2667 if (options->verbose)
2668 fprintf(stdout, format: "Checking if application binary is in package.\n");
2669
2670 QString applicationFileName = "lib%1_%2.so"_L1.arg(args&: options->applicationBinary,
2671 args&: options->currentArchitecture);
2672
2673 QString applicationPath = "%1/libs/%2/%3"_L1.arg(args&: options->outputDirectory,
2674 args&: options->currentArchitecture,
2675 args&: applicationFileName);
2676 if (!QFile::exists(fileName: applicationPath)) {
2677#if defined(Q_OS_WIN32)
2678 const auto makeTool = "mingw32-make"_L1; // Only Mingw host builds supported on Windows currently
2679#else
2680 const auto makeTool = "make"_L1;
2681#endif
2682 fprintf(stderr, format: "Application binary is not in output directory: %s. Please run '%s install INSTALL_ROOT=%s' first.\n",
2683 qPrintable(applicationFileName),
2684 qPrintable(makeTool),
2685 qPrintable(options->outputDirectory));
2686 return false;
2687 }
2688 return true;
2689}
2690
2691auto runAdb(const Options &options, const QString &arguments)
2692 -> decltype(openProcess(command: {}))
2693{
2694 QString adb = execSuffixAppended(path: options.sdkPath + "/platform-tools/adb"_L1);
2695 if (!QFile::exists(fileName: adb)) {
2696 fprintf(stderr, format: "Cannot find adb tool: %s\n", qPrintable(adb));
2697 return 0;
2698 }
2699 QString installOption;
2700 if (!options.installLocation.isEmpty())
2701 installOption = " -s "_L1 + shellQuote(arg: options.installLocation);
2702
2703 adb = "%1%2 %3"_L1.arg(args: shellQuote(arg: adb), args&: installOption, args: arguments);
2704
2705 if (options.verbose)
2706 fprintf(stdout, format: "Running command \"%s\"\n", adb.toLocal8Bit().constData());
2707
2708 auto adbCommand = openProcess(command: adb);
2709 if (adbCommand == 0) {
2710 fprintf(stderr, format: "Cannot start adb: %s\n", qPrintable(adb));
2711 return 0;
2712 }
2713
2714 return adbCommand;
2715}
2716
2717bool goodToCopy(const Options *options, const QString &file, QStringList *unmetDependencies)
2718{
2719 if (!file.endsWith(s: ".so"_L1))
2720 return true;
2721
2722 if (!checkArchitecture(options: *options, fileName: file))
2723 return false;
2724
2725 bool ret = true;
2726 const auto libs = getQtLibsFromElf(options: *options, fileName: file);
2727 for (const QString &lib : libs) {
2728 if (!options->qtDependencies[options->currentArchitecture].contains(t: QtDependency(lib, absoluteFilePath(options, relativeFileName: lib)))) {
2729 ret = false;
2730 unmetDependencies->append(t: lib);
2731 }
2732 }
2733
2734 return ret;
2735}
2736
2737bool copyQtFiles(Options *options)
2738{
2739 if (options->verbose) {
2740 switch (options->deploymentMechanism) {
2741 case Options::Bundled:
2742 fprintf(stdout, format: "Copying %zd dependencies from Qt into package.\n", size_t(options->qtDependencies[options->currentArchitecture].size()));
2743 break;
2744 case Options::Unbundled:
2745 fprintf(stdout, format: "Copying dependencies from Qt into the package build folder,"
2746 "skipping native libraries.\n");
2747 break;
2748 };
2749 }
2750
2751 if (!options->build)
2752 return true;
2753
2754
2755 QString libsDirectory = "libs/"_L1;
2756
2757 // Copy other Qt dependencies
2758 auto assetsDestinationDirectory = "assets/android_rcc_bundle/"_L1;
2759 for (const QtDependency &qtDependency : std::as_const(t&: options->qtDependencies[options->currentArchitecture])) {
2760 QString sourceFileName = qtDependency.absolutePath;
2761 QString destinationFileName;
2762 bool isSharedLibrary = qtDependency.relativePath.endsWith(s: ".so"_L1);
2763 if (isSharedLibrary) {
2764 QString garbledFileName = qtDependency.relativePath.mid(
2765 position: qtDependency.relativePath.lastIndexOf(c: u'/') + 1);
2766 destinationFileName = libsDirectory + options->currentArchitecture + u'/' + garbledFileName;
2767 } else if (QDir::fromNativeSeparators(pathName: qtDependency.relativePath).startsWith(s: "jar/"_L1)) {
2768 destinationFileName = libsDirectory + qtDependency.relativePath.mid(position: sizeof("jar/") - 1);
2769 } else {
2770 destinationFileName = assetsDestinationDirectory + qtDependency.relativePath;
2771 }
2772
2773 if (!QFile::exists(fileName: sourceFileName)) {
2774 fprintf(stderr, format: "Source Qt file does not exist: %s.\n", qPrintable(sourceFileName));
2775 return false;
2776 }
2777
2778 QStringList unmetDependencies;
2779 if (!goodToCopy(options, file: sourceFileName, unmetDependencies: &unmetDependencies)) {
2780 if (unmetDependencies.isEmpty()) {
2781 if (options->verbose) {
2782 fprintf(stdout, format: " -- Skipping %s, architecture mismatch.\n",
2783 qPrintable(sourceFileName));
2784 }
2785 } else {
2786 fprintf(stdout, format: " -- Skipping %s. It has unmet dependencies: %s.\n",
2787 qPrintable(sourceFileName),
2788 qPrintable(unmetDependencies.join(u',')));
2789 }
2790 continue;
2791 }
2792
2793 if ((isDeployment(options, deployment: Options::Bundled) || !isSharedLibrary)
2794 && !copyFileIfNewer(sourceFileName,
2795 destinationFileName: options->outputDirectory + u'/' + destinationFileName,
2796 options: *options)) {
2797 return false;
2798 }
2799 options->bundledFiles[options->currentArchitecture] += std::make_pair(x&: destinationFileName, y: qtDependency.relativePath);
2800 }
2801
2802 return true;
2803}
2804
2805QStringList getLibraryProjectsInOutputFolder(const Options &options)
2806{
2807 QStringList ret;
2808
2809 QFile file(options.outputDirectory + "/project.properties"_L1);
2810 if (file.open(flags: QIODevice::ReadOnly)) {
2811 while (!file.atEnd()) {
2812 QByteArray line = file.readLine().trimmed();
2813 if (line.startsWith(bv: "android.library.reference")) {
2814 int equalSignIndex = line.indexOf(ch: '=');
2815 if (equalSignIndex >= 0) {
2816 QString path = QString::fromLocal8Bit(ba: line.mid(index: equalSignIndex + 1));
2817
2818 QFileInfo info(options.outputDirectory + u'/' + path);
2819 if (QDir::isRelativePath(path)
2820 && info.exists()
2821 && info.isDir()
2822 && info.canonicalFilePath().startsWith(s: options.outputDirectory)) {
2823 ret += info.canonicalFilePath();
2824 }
2825 }
2826 }
2827 }
2828 }
2829
2830 return ret;
2831}
2832
2833QString findInPath(const QString &fileName)
2834{
2835 const QString path = QString::fromLocal8Bit(ba: qgetenv(varName: "PATH"));
2836#if defined(Q_OS_WIN32)
2837 QLatin1Char separator(';');
2838#else
2839 QLatin1Char separator(':');
2840#endif
2841
2842 const QStringList paths = path.split(sep: separator);
2843 for (const QString &path : paths) {
2844 QFileInfo fileInfo(path + u'/' + fileName);
2845 if (fileInfo.exists() && fileInfo.isFile() && fileInfo.isExecutable())
2846 return path + u'/' + fileName;
2847 }
2848
2849 return QString();
2850}
2851
2852typedef QMap<QByteArray, QByteArray> GradleProperties;
2853
2854static GradleProperties readGradleProperties(const QString &path)
2855{
2856 GradleProperties properties;
2857 QFile file(path);
2858 if (!file.open(flags: QIODevice::ReadOnly))
2859 return properties;
2860
2861 const auto lines = file.readAll().split(sep: '\n');
2862 for (const QByteArray &line : lines) {
2863 if (line.trimmed().startsWith(c: '#'))
2864 continue;
2865
2866 const int idx = line.indexOf(ch: '=');
2867 if (idx > -1)
2868 properties[line.left(n: idx).trimmed()] = line.mid(index: idx + 1).trimmed();
2869 }
2870 file.close();
2871 return properties;
2872}
2873
2874static bool mergeGradleProperties(const QString &path, GradleProperties properties)
2875{
2876 const QString oldPathStr = path + u'~';
2877 QFile::remove(fileName: oldPathStr);
2878 QFile::rename(oldName: path, newName: oldPathStr);
2879 QFile file(path);
2880 if (!file.open(flags: QIODevice::Truncate | QIODevice::WriteOnly | QIODevice::Text)) {
2881 fprintf(stderr, format: "Can't open file: %s for writing\n", qPrintable(file.fileName()));
2882 return false;
2883 }
2884
2885 QFile oldFile(oldPathStr);
2886 if (oldFile.open(flags: QIODevice::ReadOnly)) {
2887 while (!oldFile.atEnd()) {
2888 QByteArray line(oldFile.readLine());
2889 QList<QByteArray> prop(line.split(sep: '='));
2890 if (prop.size() > 1) {
2891 GradleProperties::iterator it = properties.find(key: prop.at(i: 0).trimmed());
2892 if (it != properties.end()) {
2893 file.write(data: it.key() + '=' + it.value() + '\n');
2894 properties.erase(it);
2895 continue;
2896 }
2897 }
2898 file.write(data: line.trimmed() + '\n');
2899 }
2900 oldFile.close();
2901 QFile::remove(fileName: oldPathStr);
2902 }
2903
2904 for (GradleProperties::const_iterator it = properties.begin(); it != properties.end(); ++it)
2905 file.write(data: it.key() + '=' + it.value() + '\n');
2906
2907 file.close();
2908 return true;
2909}
2910
2911#if defined(Q_OS_WIN32)
2912void checkAndWarnGradleLongPaths(const QString &outputDirectory)
2913{
2914 QStringList longFileNames;
2915 using F = QDirListing::IteratorFlag;
2916 for (const auto &dirEntry : QDirListing(outputDirectory, QStringList(u"*.java"_s),
2917 F::FilesOnly | F::Recursive)) {
2918 if (dirEntry.size() >= MAX_PATH)
2919 longFileNames.append(dirEntry.filePath());
2920 }
2921
2922 if (!longFileNames.isEmpty()) {
2923 fprintf(stderr,
2924 "The maximum path length that can be processed by Gradle on Windows is %d characters.\n"
2925 "Consider moving your project to reduce its path length.\n"
2926 "The following files have too long paths:\n%s.\n",
2927 MAX_PATH, qPrintable(longFileNames.join(u'\n')));
2928 }
2929}
2930#endif
2931
2932bool buildAndroidProject(const Options &options)
2933{
2934 GradleProperties localProperties;
2935 localProperties["sdk.dir"] = QDir::fromNativeSeparators(pathName: options.sdkPath).toUtf8();
2936 const QString localPropertiesPath = options.outputDirectory + "local.properties"_L1;
2937 if (!mergeGradleProperties(path: localPropertiesPath, properties: localProperties))
2938 return false;
2939
2940 const QString gradlePropertiesPath = options.outputDirectory + "gradle.properties"_L1;
2941 GradleProperties gradleProperties = readGradleProperties(path: gradlePropertiesPath);
2942
2943 const QString gradleBuildFilePath = options.outputDirectory + "build.gradle"_L1;
2944 GradleBuildConfigs gradleConfigs = gradleBuildConfigs(path: gradleBuildFilePath);
2945 if (!gradleConfigs.setsLegacyPackaging)
2946 gradleProperties["android.bundle.enableUncompressedNativeLibs"] = "false";
2947
2948 gradleProperties["buildDir"] = "build";
2949 gradleProperties["qtAndroidDir"] =
2950 (options.qtInstallDirectory + u'/' + options.qtDataDirectory +
2951 "/src/android/java"_L1)
2952 .toUtf8();
2953 // The following property "qt5AndroidDir" is only for compatibility.
2954 // Projects using a custom build.gradle file may use this variable.
2955 // ### Qt7: Remove the following line
2956 gradleProperties["qt5AndroidDir"] =
2957 (options.qtInstallDirectory + u'/' + options.qtDataDirectory +
2958 "/src/android/java"_L1)
2959 .toUtf8();
2960
2961 QByteArray sdkPlatformVersion;
2962 // Provide the integer version only if build.gradle explicitly converts to Integer,
2963 // to avoid regression to existing projects that build for sdk platform of form android-xx.
2964 if (gradleConfigs.usesIntegerCompileSdkVersion) {
2965 const QByteArray tmp = options.androidPlatform.split(sep: u'-').last().toLocal8Bit();
2966 bool ok;
2967 tmp.toInt(ok: &ok);
2968 if (ok) {
2969 sdkPlatformVersion = tmp;
2970 } else {
2971 fprintf(stderr, format: "Warning: Gradle expects SDK platform version to be an integer, "
2972 "but the set version is not convertible to an integer.");
2973 }
2974 }
2975
2976 if (sdkPlatformVersion.isEmpty())
2977 sdkPlatformVersion = options.androidPlatform.toLocal8Bit();
2978
2979 gradleProperties["androidPackageName"] = options.packageName.toLocal8Bit();
2980 gradleProperties["androidCompileSdkVersion"] = sdkPlatformVersion;
2981 gradleProperties["qtMinSdkVersion"] = options.minSdkVersion;
2982 gradleProperties["qtTargetSdkVersion"] = options.targetSdkVersion;
2983 gradleProperties["androidNdkVersion"] = options.ndkVersion.toUtf8();
2984 if (gradleProperties["androidBuildToolsVersion"].isEmpty())
2985 gradleProperties["androidBuildToolsVersion"] = options.sdkBuildToolsVersion.toLocal8Bit();
2986 QString abiList;
2987 for (auto it = options.architectures.constBegin(); it != options.architectures.constEnd(); ++it) {
2988 if (!it->enabled)
2989 continue;
2990 if (abiList.size())
2991 abiList.append(v: u",");
2992 abiList.append(s: it.key());
2993 }
2994 gradleProperties["qtTargetAbiList"] = abiList.toLocal8Bit();// armeabi-v7a or arm64-v8a or ...
2995 gradleProperties["qtGradlePluginType"] = options.buildAar
2996 ? "com.android.library"
2997 : "com.android.application";
2998 if (!mergeGradleProperties(path: gradlePropertiesPath, properties: gradleProperties))
2999 return false;
3000
3001 QString gradlePath = batSuffixAppended(path: options.outputDirectory + "gradlew"_L1);
3002#ifndef Q_OS_WIN32
3003 {
3004 QFile f(gradlePath);
3005 if (!f.setPermissions(f.permissions() | QFileDevice::ExeUser))
3006 fprintf(stderr, format: "Cannot set permissions %s\n", qPrintable(gradlePath));
3007 }
3008#endif
3009
3010 QString oldPath = QDir::currentPath();
3011 if (!QDir::setCurrent(options.outputDirectory)) {
3012 fprintf(stderr, format: "Cannot current path to %s\n", qPrintable(options.outputDirectory));
3013 return false;
3014 }
3015
3016 QString commandLine = "%1 %2"_L1.arg(args: shellQuote(arg: gradlePath), args: options.releasePackage ? " assembleRelease"_L1 : " assembleDebug"_L1);
3017 if (options.buildAAB)
3018 commandLine += " bundle"_L1;
3019
3020 if (options.verbose)
3021 commandLine += " --info"_L1;
3022
3023 auto gradleCommand = openProcess(command: commandLine);
3024 if (gradleCommand == 0) {
3025 fprintf(stderr, format: "Cannot run gradle command: %s\n.", qPrintable(commandLine));
3026 return false;
3027 }
3028
3029 char buffer[512];
3030 while (fgets(s: buffer, n: sizeof(buffer), stream: gradleCommand.get()) != nullptr) {
3031 fprintf(stdout, format: "%s", buffer);
3032 fflush(stdout);
3033 }
3034
3035 const int errorCode = pclose(stream: gradleCommand.release());
3036 if (errorCode != 0) {
3037 fprintf(stderr, format: "Building the android package failed!\n");
3038 if (!options.verbose)
3039 fprintf(stderr, format: " -- For more information, run this command with --verbose.\n");
3040
3041#if defined(Q_OS_WIN32)
3042 checkAndWarnGradleLongPaths(options.outputDirectory);
3043#endif
3044 return false;
3045 }
3046
3047 if (!QDir::setCurrent(oldPath)) {
3048 fprintf(stderr, format: "Cannot change back to old path: %s\n", qPrintable(oldPath));
3049 return false;
3050 }
3051
3052 return true;
3053}
3054
3055bool uninstallApk(const Options &options)
3056{
3057 if (options.verbose)
3058 fprintf(stdout, format: "Uninstalling old Android package %s if present.\n", qPrintable(options.packageName));
3059
3060
3061 auto adbCommand = runAdb(options, arguments: " uninstall "_L1 + shellQuote(arg: options.packageName));
3062 if (adbCommand == 0)
3063 return false;
3064
3065 if (options.verbose || mustReadOutputAnyway) {
3066 char buffer[512];
3067 while (fgets(s: buffer, n: sizeof(buffer), stream: adbCommand.get()) != nullptr)
3068 if (options.verbose)
3069 fprintf(stdout, format: "%s", buffer);
3070 }
3071
3072 const int returnCode = pclose(stream: adbCommand.release());
3073 if (returnCode != 0) {
3074 fprintf(stderr, format: "Warning: Uninstall failed!\n");
3075 if (!options.verbose)
3076 fprintf(stderr, format: " -- Run with --verbose for more information.\n");
3077 return false;
3078 }
3079
3080 return true;
3081}
3082
3083enum PackageType {
3084 AAB,
3085 AAR,
3086 UnsignedAPK,
3087 SignedAPK
3088};
3089
3090QString packagePath(const Options &options, PackageType packageType)
3091{
3092 // The package type is always AAR if option.buildAar has been set
3093 if (options.buildAar)
3094 packageType = AAR;
3095
3096 static const QHash<PackageType, QLatin1StringView> packageTypeToPath{
3097 { AAB, "bundle"_L1 }, { AAR, "aar"_L1 }, { UnsignedAPK, "apk"_L1 }, { SignedAPK, "apk"_L1 }
3098 };
3099 static const QHash<PackageType, QLatin1StringView> packageTypeToExtension{
3100 { AAB, "aab"_L1 }, { AAR, "aar"_L1 }, { UnsignedAPK, "apk"_L1 }, { SignedAPK, "apk"_L1 }
3101 };
3102
3103 const QString buildType(options.releasePackage ? "release"_L1 : "debug"_L1);
3104 QString signedSuffix;
3105 if (packageType == SignedAPK)
3106 signedSuffix = "-signed"_L1;
3107 else if (packageType == UnsignedAPK && options.releasePackage)
3108 signedSuffix = "-unsigned"_L1;
3109
3110 QString dirPath(options.outputDirectory);
3111 dirPath += "/build/outputs/%1/"_L1.arg(args: packageTypeToPath[packageType]);
3112 if (QDir(dirPath + buildType).exists())
3113 dirPath += buildType;
3114
3115 const QString fileName = "/%1-%2%3.%4"_L1.arg(
3116 args: QDir(options.outputDirectory).dirName(),
3117 args: buildType,
3118 args&: signedSuffix,
3119 args: packageTypeToExtension[packageType]);
3120
3121 return dirPath + fileName;
3122}
3123
3124bool installApk(const Options &options)
3125{
3126 fflush(stdout);
3127 // Uninstall if necessary
3128 if (options.uninstallApk)
3129 uninstallApk(options);
3130
3131 if (options.verbose)
3132 fprintf(stdout, format: "Installing Android package to device.\n");
3133
3134 auto adbCommand = runAdb(options, arguments: " install -r "_L1
3135 + packagePath(options, packageType: options.keyStore.isEmpty() ? UnsignedAPK
3136 : SignedAPK));
3137 if (adbCommand == 0)
3138 return false;
3139
3140 if (options.verbose || mustReadOutputAnyway) {
3141 char buffer[512];
3142 while (fgets(s: buffer, n: sizeof(buffer), stream: adbCommand.get()) != nullptr)
3143 if (options.verbose)
3144 fprintf(stdout, format: "%s", buffer);
3145 }
3146
3147 const int returnCode = pclose(stream: adbCommand.release());
3148 if (returnCode != 0) {
3149 fprintf(stderr, format: "Installing to device failed!\n");
3150 if (!options.verbose)
3151 fprintf(stderr, format: " -- Run with --verbose for more information.\n");
3152 return false;
3153 }
3154
3155 return true;
3156}
3157
3158bool copyPackage(const Options &options)
3159{
3160 fflush(stdout);
3161 auto from = packagePath(options, packageType: options.keyStore.isEmpty() ? UnsignedAPK : SignedAPK);
3162 QFile::remove(fileName: options.apkPath);
3163 return QFile::copy(fileName: from, newName: options.apkPath);
3164}
3165
3166bool copyStdCpp(Options *options)
3167{
3168 if (isDeployment(options, deployment: Options::Unbundled))
3169 return true;
3170 if (options->verbose)
3171 fprintf(stdout, format: "Copying STL library\n");
3172
3173 const QString triple = options->architectures[options->currentArchitecture].triple;
3174 const QString stdCppPath = "%1/%2/lib%3.so"_L1.arg(args&: options->stdCppPath, args: triple,
3175 args&: options->stdCppName);
3176 if (!QFile::exists(fileName: stdCppPath)) {
3177 fprintf(stderr, format: "STL library does not exist at %s\n", qPrintable(stdCppPath));
3178 fflush(stdout);
3179 fflush(stderr);
3180 return false;
3181 }
3182
3183 const QString destinationFile = "%1/libs/%2/lib%3.so"_L1.arg(args&: options->outputDirectory,
3184 args&: options->currentArchitecture,
3185 args&: options->stdCppName);
3186 return copyFileIfNewer(sourceFileName: stdCppPath, destinationFileName: destinationFile, options: *options);
3187}
3188
3189static QString zipalignPath(const Options &options, bool *ok)
3190{
3191 *ok = true;
3192 QString zipAlignTool = execSuffixAppended(path: options.sdkPath + "/tools/zipalign"_L1);
3193 if (!QFile::exists(fileName: zipAlignTool)) {
3194 zipAlignTool = execSuffixAppended(path: options.sdkPath + "/build-tools/"_L1 +
3195 options.sdkBuildToolsVersion + "/zipalign"_L1);
3196 if (!QFile::exists(fileName: zipAlignTool)) {
3197 fprintf(stderr, format: "zipalign tool not found: %s\n", qPrintable(zipAlignTool));
3198 *ok = false;
3199 }
3200 }
3201
3202 return zipAlignTool;
3203}
3204
3205bool signAAB(const Options &options)
3206{
3207 if (options.verbose)
3208 fprintf(stdout, format: "Signing Android package.\n");
3209
3210 QString jdkPath = options.jdkPath;
3211
3212 if (jdkPath.isEmpty())
3213 jdkPath = QString::fromLocal8Bit(ba: qgetenv(varName: "JAVA_HOME"));
3214
3215 QString jarSignerTool = execSuffixAppended(path: "jarsigner"_L1);
3216 if (jdkPath.isEmpty() || !QFile::exists(fileName: jdkPath + "/bin/"_L1 + jarSignerTool))
3217 jarSignerTool = findInPath(fileName: jarSignerTool);
3218 else
3219 jarSignerTool = jdkPath + "/bin/"_L1 + jarSignerTool;
3220
3221 if (!QFile::exists(fileName: jarSignerTool)) {
3222 fprintf(stderr, format: "Cannot find jarsigner in JAVA_HOME or PATH. Please use --jdk option to pass in the correct path to JDK.\n");
3223 return false;
3224 }
3225
3226 jarSignerTool = "%1 -sigalg %2 -digestalg %3 -keystore %4"_L1
3227 .arg(args: shellQuote(arg: jarSignerTool), args: shellQuote(arg: options.sigAlg), args: shellQuote(arg: options.digestAlg), args: shellQuote(arg: options.keyStore));
3228
3229 if (!options.keyStorePassword.isEmpty())
3230 jarSignerTool += " -storepass %1"_L1.arg(args: shellQuote(arg: options.keyStorePassword));
3231
3232 if (!options.storeType.isEmpty())
3233 jarSignerTool += " -storetype %1"_L1.arg(args: shellQuote(arg: options.storeType));
3234
3235 if (!options.keyPass.isEmpty())
3236 jarSignerTool += " -keypass %1"_L1.arg(args: shellQuote(arg: options.keyPass));
3237
3238 if (!options.sigFile.isEmpty())
3239 jarSignerTool += " -sigfile %1"_L1.arg(args: shellQuote(arg: options.sigFile));
3240
3241 if (!options.signedJar.isEmpty())
3242 jarSignerTool += " -signedjar %1"_L1.arg(args: shellQuote(arg: options.signedJar));
3243
3244 if (!options.tsaUrl.isEmpty())
3245 jarSignerTool += " -tsa %1"_L1.arg(args: shellQuote(arg: options.tsaUrl));
3246
3247 if (!options.tsaCert.isEmpty())
3248 jarSignerTool += " -tsacert %1"_L1.arg(args: shellQuote(arg: options.tsaCert));
3249
3250 if (options.internalSf)
3251 jarSignerTool += " -internalsf"_L1;
3252
3253 if (options.sectionsOnly)
3254 jarSignerTool += " -sectionsonly"_L1;
3255
3256 if (options.protectedAuthenticationPath)
3257 jarSignerTool += " -protected"_L1;
3258
3259 auto jarSignPackage = [&](const QString &file) {
3260 fprintf(stdout, format: "Signing file %s\n", qPrintable(file));
3261 fflush(stdout);
3262 QString command = jarSignerTool + " %1 %2"_L1.arg(args: shellQuote(arg: file))
3263 .arg(a: shellQuote(arg: options.keyStoreAlias));
3264
3265 auto jarSignerCommand = openProcess(command);
3266 if (jarSignerCommand == 0) {
3267 fprintf(stderr, format: "Couldn't run jarsigner.\n");
3268 return false;
3269 }
3270
3271 if (options.verbose) {
3272 char buffer[512];
3273 while (fgets(s: buffer, n: sizeof(buffer), stream: jarSignerCommand.get()) != nullptr)
3274 fprintf(stdout, format: "%s", buffer);
3275 }
3276
3277 const int errorCode = pclose(stream: jarSignerCommand.release());
3278 if (errorCode != 0) {
3279 fprintf(stderr, format: "jarsigner command failed.\n");
3280 if (!options.verbose)
3281 fprintf(stderr, format: " -- Run with --verbose for more information.\n");
3282 return false;
3283 }
3284 return true;
3285 };
3286
3287 if (options.buildAAB && !jarSignPackage(packagePath(options, packageType: AAB)))
3288 return false;
3289 return true;
3290}
3291
3292bool signPackage(const Options &options)
3293{
3294 const QString apksignerTool = batSuffixAppended(path: options.sdkPath + "/build-tools/"_L1 +
3295 options.sdkBuildToolsVersion + "/apksigner"_L1);
3296 // APKs signed with apksigner must not be changed after they're signed,
3297 // therefore we need to zipalign it before we sign it.
3298
3299 bool ok;
3300 QString zipAlignTool = zipalignPath(options, ok: &ok);
3301 if (!ok)
3302 return false;
3303
3304 auto zipalignRunner = [](const QString &zipAlignCommandLine) {
3305 auto zipAlignCommand = openProcess(command: zipAlignCommandLine);
3306 if (zipAlignCommand == 0) {
3307 fprintf(stderr, format: "Couldn't run zipalign.\n");
3308 return false;
3309 }
3310
3311 char buffer[512];
3312 while (fgets(s: buffer, n: sizeof(buffer), stream: zipAlignCommand.get()) != nullptr)
3313 fprintf(stdout, format: "%s", buffer);
3314
3315 return pclose(stream: zipAlignCommand.release()) == 0;
3316 };
3317
3318 const QString verifyZipAlignCommandLine =
3319 "%1%2 -c 4 %3"_L1
3320 .arg(args: shellQuote(arg: zipAlignTool),
3321 args: options.verbose ? " -v"_L1 : QLatin1StringView(),
3322 args: shellQuote(arg: packagePath(options, packageType: UnsignedAPK)));
3323
3324 if (zipalignRunner(verifyZipAlignCommandLine)) {
3325 if (options.verbose)
3326 fprintf(stdout, format: "APK already aligned, copying it for signing.\n");
3327
3328 if (QFile::exists(fileName: packagePath(options, packageType: SignedAPK)))
3329 QFile::remove(fileName: packagePath(options, packageType: SignedAPK));
3330
3331 if (!QFile::copy(fileName: packagePath(options, packageType: UnsignedAPK), newName: packagePath(options, packageType: SignedAPK))) {
3332 fprintf(stderr, format: "Could not copy unsigned APK.\n");
3333 return false;
3334 }
3335 } else {
3336 if (options.verbose)
3337 fprintf(stdout, format: "APK not aligned, aligning it for signing.\n");
3338
3339 const QString zipAlignCommandLine =
3340 "%1%2 -f 4 %3 %4"_L1
3341 .arg(args: shellQuote(arg: zipAlignTool),
3342 args: options.verbose ? " -v"_L1 : QLatin1StringView(),
3343 args: shellQuote(arg: packagePath(options, packageType: UnsignedAPK)),
3344 args: shellQuote(arg: packagePath(options, packageType: SignedAPK)));
3345
3346 if (!zipalignRunner(zipAlignCommandLine)) {
3347 fprintf(stderr, format: "zipalign command failed.\n");
3348 if (!options.verbose)
3349 fprintf(stderr, format: " -- Run with --verbose for more information.\n");
3350 return false;
3351 }
3352 }
3353
3354 QString apkSignCommand = "%1 sign --ks %2"_L1
3355 .arg(args: shellQuote(arg: apksignerTool), args: shellQuote(arg: options.keyStore));
3356
3357 if (!options.keyStorePassword.isEmpty())
3358 apkSignCommand += " --ks-pass pass:%1"_L1.arg(args: shellQuote(arg: options.keyStorePassword));
3359
3360 if (!options.keyStoreAlias.isEmpty())
3361 apkSignCommand += " --ks-key-alias %1"_L1.arg(args: shellQuote(arg: options.keyStoreAlias));
3362
3363 if (!options.keyPass.isEmpty())
3364 apkSignCommand += " --key-pass pass:%1"_L1.arg(args: shellQuote(arg: options.keyPass));
3365
3366 if (options.verbose)
3367 apkSignCommand += " --verbose"_L1;
3368
3369 apkSignCommand += " %1"_L1.arg(args: shellQuote(arg: packagePath(options, packageType: SignedAPK)));
3370
3371 auto apkSignerRunner = [](const QString &command, bool verbose) {
3372 auto apkSigner = openProcess(command);
3373 if (apkSigner == 0) {
3374 fprintf(stderr, format: "Couldn't run apksigner.\n");
3375 return false;
3376 }
3377
3378 char buffer[512];
3379 while (fgets(s: buffer, n: sizeof(buffer), stream: apkSigner.get()) != nullptr)
3380 fprintf(stdout, format: "%s", buffer);
3381
3382 const int errorCode = pclose(stream: apkSigner.release());
3383 if (errorCode != 0) {
3384 fprintf(stderr, format: "apksigner command failed.\n");
3385 if (!verbose)
3386 fprintf(stderr, format: " -- Run with --verbose for more information.\n");
3387 return false;
3388 }
3389 return true;
3390 };
3391
3392 // Sign the package
3393 if (!apkSignerRunner(apkSignCommand, options.verbose))
3394 return false;
3395
3396 const QString apkVerifyCommand =
3397 "%1 verify --verbose %2"_L1
3398 .arg(args: shellQuote(arg: apksignerTool), args: shellQuote(arg: packagePath(options, packageType: SignedAPK)));
3399
3400 if (options.buildAAB && !signAAB(options))
3401 return false;
3402
3403 // Verify the package and remove the unsigned apk
3404 return apkSignerRunner(apkVerifyCommand, true) && QFile::remove(fileName: packagePath(options, packageType: UnsignedAPK));
3405}
3406
3407enum ErrorCode
3408{
3409 Success,
3410 SyntaxErrorOrHelpRequested = 1,
3411 CannotReadInputFile = 2,
3412 CannotCopyAndroidTemplate = 3,
3413 CannotReadDependencies = 4,
3414 CannotCopyGnuStl = 5,
3415 CannotCopyQtFiles = 6,
3416 CannotFindApplicationBinary = 7,
3417 CannotCopyAndroidExtraLibs = 10,
3418 CannotCopyAndroidSources = 11,
3419 CannotUpdateAndroidFiles = 12,
3420 CannotCreateAndroidProject = 13, // Not used anymore
3421 CannotBuildAndroidProject = 14,
3422 CannotSignPackage = 15,
3423 CannotInstallApk = 16,
3424 CannotCopyAndroidExtraResources = 19,
3425 CannotCopyApk = 20,
3426 CannotCreateRcc = 21,
3427 CannotGenerateJavaQmlComponents = 22
3428};
3429
3430bool writeDependencyFile(const Options &options)
3431{
3432 if (options.verbose)
3433 fprintf(stdout, format: "Writing dependency file.\n");
3434
3435 QString relativeTargetPath;
3436 if (options.copyDependenciesOnly) {
3437 // When androiddeploy Qt is running in copyDependenciesOnly mode we need to use
3438 // the timestamp file as the target to collect dependencies.
3439 QString timestampAbsPath = QFileInfo(options.depFilePath).absolutePath() + "/timestamp"_L1;
3440 relativeTargetPath = QDir(options.buildDirectory).relativeFilePath(fileName: timestampAbsPath);
3441 } else {
3442 relativeTargetPath = QDir(options.buildDirectory).relativeFilePath(fileName: options.apkPath);
3443 }
3444
3445 QFile depFile(options.depFilePath);
3446 if (depFile.open(flags: QIODevice::WriteOnly)) {
3447 depFile.write(data: escapeAndEncodeDependencyPath(path: relativeTargetPath));
3448 depFile.write(data: ": ");
3449
3450 for (const auto &file : dependenciesForDepfile) {
3451 depFile.write(data: " \\\n ");
3452 depFile.write(data: escapeAndEncodeDependencyPath(path: file));
3453 }
3454
3455 depFile.write(data: "\n");
3456 }
3457 return true;
3458}
3459
3460int generateJavaQmlComponents(const Options &options)
3461{
3462 const auto firstCharToUpper = [](const QString &str) -> QString {
3463 if (str.isEmpty())
3464 return str;
3465 return str.left(n: 1).toUpper() + str.mid(position: 1);
3466 };
3467
3468 const auto upperFirstAndAfterDot = [](QString str) -> QString {
3469 if (str.isEmpty())
3470 return str;
3471
3472 str[0] = str[0].toUpper();
3473
3474 for (int i = 0; i < str.size(); ++i) {
3475 if (str[i] == "."_L1) {
3476 // Move to the next character after the dot
3477 int j = i + 1;
3478 if (j < str.size()) {
3479 str[j] = str[j].toUpper();
3480 }
3481 }
3482 }
3483 return str;
3484 };
3485
3486 const auto getImportPaths = [options](const QString &buildPath, const QString &libName,
3487 QStringList &appImports, QStringList &externalImports) -> bool {
3488 QFile confRspFile("%1/.qt/qml_imports/%2_conf.rsp"_L1.arg(args: buildPath, args: libName));
3489 if (!confRspFile.exists() || !confRspFile.open(flags: QFile::ReadOnly))
3490 return false;
3491 QTextStream rspStream(&confRspFile);
3492 while (!rspStream.atEnd()) {
3493 QString currentLine = rspStream.readLine();
3494 if (currentLine.compare(other: "-importPath"_L1) == 0) {
3495 currentLine = rspStream.readLine();
3496 if (QDir::cleanPath(path: currentLine).startsWith(s: QDir::cleanPath(path: buildPath)))
3497 appImports << currentLine;
3498 else
3499 externalImports << currentLine;
3500 }
3501 }
3502
3503 // Find inner qmldir files
3504 QSet<QString> qmldirDirectories;
3505 for (const QString &path : appImports) {
3506 QDirIterator it(path, QDir::Dirs | QDir::NoDotAndDotDot, QDirIterator::Subdirectories);
3507 while (it.hasNext()) {
3508 const QDir dir(it.next());
3509 const QString absolutePath = dir.absolutePath();
3510 if (!absolutePath.startsWith(s: options.outputDirectory)
3511 && dir.exists(name: "qmldir"_L1)) {
3512 qmldirDirectories.insert(value: absolutePath);
3513 }
3514 }
3515 }
3516 appImports << qmldirDirectories.values();
3517
3518 return appImports.count() + externalImports.count();
3519 };
3520
3521 struct ComponentInfo {
3522 QString name;
3523 QString path;
3524 };
3525
3526 struct ModuleInfo
3527 {
3528 QString moduleName;
3529 QString preferPath;
3530 QList<ComponentInfo> qmlComponents;
3531 bool isValid() { return qmlComponents.size() && moduleName.size(); }
3532 };
3533
3534 const auto getModuleInfo = [](const QString &qmldirPath) -> ModuleInfo {
3535 QFile qmlDirFile(qmldirPath + "/qmldir"_L1);
3536 if (!qmlDirFile.exists() || !qmlDirFile.open(flags: QFile::ReadOnly))
3537 return ModuleInfo();
3538 ModuleInfo moduleInfo;
3539 QSet<QString> qmlComponentNames;
3540 QTextStream qmldirStream(&qmlDirFile);
3541 while (!qmldirStream.atEnd()) {
3542 const QString currentLine = qmldirStream.readLine();
3543 if (currentLine.size() && currentLine[0].isLower()) {
3544 // TODO QTBUG-125891: Handling of QML modules with dotted URI
3545 if (currentLine.startsWith(s: "module "_L1))
3546 moduleInfo.moduleName = currentLine.split(sep: " "_L1)[1];
3547 else if (currentLine.startsWith(s: "prefer "_L1))
3548 moduleInfo.preferPath = currentLine.split(sep: " "_L1)[1];
3549 } else if (currentLine.size()
3550 && (currentLine[0].isUpper() || currentLine.startsWith(s: "singleton"_L1))) {
3551 const QStringList parts = currentLine.split(sep: " "_L1);
3552 if (parts.size() > 2 && !qmlComponentNames.contains(value: parts.first())) {
3553 moduleInfo.qmlComponents.append(t: { .name: parts.first(), .path: parts.last() });
3554 qmlComponentNames.insert(value: parts.first());
3555 }
3556 }
3557 }
3558 return moduleInfo;
3559 };
3560
3561 const auto extractDomInfo = [](const QString &qmlDomExecPath, const QString &qmldirPath,
3562 const QString &qmlFile,
3563 const QStringList &otherImportPaths) -> QJsonObject {
3564 QByteArray domInfo;
3565#if QT_CONFIG(process)
3566 QStringList qmlDomArgs {"-d"_L1, "-D"_L1, "required"_L1, "-f"_L1, "+:propertyInfos"_L1 };
3567 for (auto &importPath : otherImportPaths)
3568 qmlDomArgs << "-I"_L1 << importPath;
3569 qmlDomArgs << "%1/%2"_L1.arg(args: qmldirPath, args: qmlFile);
3570 const QString qmlDomCmd = "%1 %2"_L1.arg(args: qmlDomExecPath, args: qmlDomArgs.join(sep: u' '));
3571 QProcess process;
3572 process.start(program: qmlDomExecPath, arguments: qmlDomArgs);
3573 if (!process.waitForStarted()) {
3574 fprintf(stderr, format: "Cannot execute command %s\n", qPrintable(qmlDomCmd));
3575 return QJsonObject();
3576 }
3577 // Wait, maximum 30 seconds
3578 if (!process.waitForFinished(msecs: 30000)) {
3579 fprintf(stderr, format: "Execution of command %s timed out.\n", qPrintable(qmlDomCmd));
3580 return QJsonObject();
3581 }
3582 domInfo = process.readAllStandardOutput();
3583
3584 QJsonParseError jsonError;
3585 const QJsonDocument jsonDoc = QJsonDocument::fromJson(json: domInfo, error: &jsonError);
3586 if (jsonError.error != QJsonParseError::NoError)
3587 fprintf(stderr, format: "Output of %s is not valid JSON document.", qPrintable(qmlDomCmd));
3588 return jsonDoc.object();
3589#else
3590#warning Generating QtQuickView Java Contents is not possible with missing QProcess feature.
3591 return QJsonObject();
3592#endif
3593 };
3594
3595 const auto getComponent = [](const QJsonObject &dom) -> QJsonObject {
3596 if (dom.isEmpty())
3597 return QJsonObject();
3598
3599 const QJsonObject currentItem = dom.value(key: "currentItem"_L1).toObject();
3600 if (!currentItem.value(key: "isValid"_L1).toBool(defaultValue: false))
3601 return QJsonObject();
3602
3603 const QJsonArray components =
3604 currentItem.value(key: "components"_L1).toObject().value(key: ""_L1).toArray();
3605 if (components.isEmpty())
3606 return QJsonObject();
3607 return components.constBegin()->toObject();
3608 };
3609
3610 const auto getProperties = [](const QJsonObject &component) -> QJsonArray {
3611 QJsonArray properties;
3612 const QJsonArray objects = component.value(key: "objects"_L1).toArray();
3613 if (objects.isEmpty())
3614 return QJsonArray();
3615 const QJsonObject propertiesObject =
3616 objects[0].toObject().value(key: "propertyInfos"_L1).toObject();
3617 for (const auto &jsonProperty : propertiesObject) {
3618 const QJsonArray propertyDefs =
3619 jsonProperty.toObject().value(key: "propertyDefs"_L1).toArray();
3620 if (propertyDefs.isEmpty())
3621 continue;
3622
3623 properties.append(value: propertyDefs[0].toObject());
3624 }
3625 return properties;
3626 };
3627
3628 const auto getMethods = [](const QJsonObject &component) -> QJsonArray {
3629 QJsonArray methods;
3630 const QJsonArray objects = component.value(key: "objects"_L1).toArray();
3631 if (objects.isEmpty())
3632 return QJsonArray();
3633 const QJsonObject methodsObject = objects[0].toObject().value(key: "methods"_L1).toObject();
3634 for (const auto &jsonMethod : methodsObject) {
3635 const QJsonArray overloads = jsonMethod.toArray();
3636 for (const auto &m : overloads)
3637 methods.append(value: m);
3638 }
3639 return methods;
3640 };
3641
3642 const static QHash<QString, QString> qmlToJavaType = {
3643 { "qreal"_L1, "Double"_L1 }, { "double"_L1, "Double"_L1 }, { "int"_L1, "Integer"_L1 },
3644 { "float"_L1, "Float"_L1 }, { "bool"_L1, "Boolean"_L1 }, { "string"_L1, "String"_L1 },
3645 { "void"_L1, "Void"_L1 }
3646 };
3647
3648 const auto endBlock = [](QTextStream &stream, int indentWidth = 0) {
3649 stream << QString(indentWidth, u' ') << "}\n";
3650 };
3651
3652 const auto createHeaderBlock = [](QTextStream &stream, const QString &javaPackage) {
3653 stream << "/* This file is autogenerated by androiddeployqt. Do not edit */\n\n"
3654 << "package %1;\n\n"_L1.arg(args: javaPackage)
3655 << "import org.qtproject.qt.android.QtSignalListener;\n"
3656 << "import org.qtproject.qt.android.QtQuickViewContent;\n\n";
3657 };
3658
3659 const auto beginComponentBlock = [](QTextStream &stream, const QString &libName,
3660 const QString &moduleName, const QString &preferPath,
3661 const ComponentInfo &componentInfo, int indentWidth = 8) {
3662 const QString indent(indentWidth, u' ');
3663
3664 stream << indent
3665 << "public final class %1 extends QtQuickViewContent {\n"_L1
3666 .arg(args: componentInfo.name)
3667 << indent << " @Override public String getLibraryName() {\n"_L1
3668 << indent << " return \"%1\";\n"_L1.arg(args: libName)
3669 << indent << " }\n"_L1
3670 << indent << " @Override public String getModuleName() {\n"_L1
3671 << indent << " return \"%1\";\n"_L1.arg(args: moduleName)
3672 << indent << " }\n"_L1
3673 << indent << " @Override public String getFilePath() {\n"_L1
3674 << indent << " return \"qrc%1%2\";\n"_L1.arg(args: preferPath)
3675 .arg(a: componentInfo.path)
3676 << indent << " }\n"_L1;
3677 };
3678
3679 const auto beginPropertyBlock = [firstCharToUpper](QTextStream &stream,
3680 const QJsonObject &propertyData,
3681 int indentWidth = 8) {
3682 const QString indent(indentWidth, u' ');
3683 const QString propertyName = propertyData["name"_L1].toString();
3684 if (propertyName.isEmpty())
3685 return;
3686 const QString upperPropertyName = firstCharToUpper(propertyName);
3687 const QString typeName = propertyData["typeName"_L1].toString();
3688 const bool isReadyonly = propertyData["isReadonly"_L1].toBool();
3689
3690 const QString javaTypeName = qmlToJavaType.value(key: typeName, defaultValue: "Object"_L1);
3691
3692 if (!isReadyonly) {
3693 stream << indent
3694 << "public void set%1(%2 %3) { setProperty(\"%3\", %3); }\n"_L1.arg(
3695 args: upperPropertyName, args: javaTypeName, args: propertyName);
3696 }
3697
3698 stream << indent
3699 << "public %2 get%1() { return this.<%2>getProperty(\"%3\"); }\n"_L1
3700 .arg(args: upperPropertyName, args: javaTypeName, args: propertyName)
3701 << indent
3702 << "public int connect%1ChangeListener(QtSignalListener<%2> signalListener) {\n"_L1
3703 .arg(args: upperPropertyName, args: javaTypeName)
3704 << indent
3705 << " return connectSignalListener(\"%1\", %2.class, signalListener);\n"_L1.arg(
3706 args: propertyName, args: javaTypeName)
3707 << indent << "}\n";
3708 };
3709
3710 const auto beginSignalBlock = [firstCharToUpper](QTextStream &stream,
3711 const QJsonObject &methodData,
3712 int indentWidth = 8) {
3713 const QString indent(indentWidth, u' ');
3714 if (methodData["methodType"_L1] != 0)
3715 return;
3716 const QJsonArray parameters = methodData["parameters"_L1].toArray();
3717 if (parameters.size() > 1)
3718 return;
3719
3720 const QString methodName = methodData["name"_L1].toString();
3721 if (methodName.isEmpty())
3722 return;
3723 const QString upperMethodName = firstCharToUpper(methodName);
3724 const QString typeName = !parameters.isEmpty()
3725 ? parameters[0].toObject()["typeName"_L1].toString()
3726 : "void"_L1;
3727
3728 const QString javaTypeName = qmlToJavaType.value(key: typeName, defaultValue: "Object"_L1);
3729 stream << indent
3730 << "public int connect%1Listener(QtSignalListener<%2> signalListener) {\n"_L1.arg(
3731 args: upperMethodName, args: javaTypeName)
3732 << indent
3733 << " return connectSignalListener(\"%1\", %2.class, signalListener);\n"_L1.arg(
3734 args: methodName, args: javaTypeName)
3735 << indent << "}\n";
3736 };
3737
3738 constexpr static auto markerFileName = "qml_java_contents"_L1;
3739 const QString libName(options.applicationBinary);
3740 QString javaPackageBase = options.packageName;
3741 const QString expectedBaseLeaf = ".%1"_L1.arg(args: libName);
3742 if (!javaPackageBase.endsWith(s: expectedBaseLeaf))
3743 javaPackageBase += expectedBaseLeaf;
3744 const QString baseSourceDir = "%1/src/%2"_L1.arg(args: options.outputDirectory,
3745 args&: QString(javaPackageBase).replace(before: u'.', after: u'/'));
3746 const QString buildPath(QDir(options.buildDirectory).absolutePath());
3747 const QString domBinaryPath(options.qmlDomBinaryPath);
3748
3749 fprintf(stdout, format: "Generating Java QML Components in %s directory.\n", qPrintable(baseSourceDir));
3750 if (!QDir().current().mkpath(dirPath: baseSourceDir)) {
3751 fprintf(stderr, format: "Cannot create %s directory\n", qPrintable(baseSourceDir));
3752 return false;
3753 }
3754
3755 QStringList appImports;
3756 QStringList externalImports;
3757 if (!getImportPaths(buildPath, libName, appImports, externalImports))
3758 return false;
3759
3760 // Remove previous directories generated by this code generator
3761 {
3762 const QString srcDir = "%1/src"_L1.arg(args: options.outputDirectory);
3763 QDirIterator iter(srcDir, { markerFileName }, QDir::Files, QDirIterator::Subdirectories);
3764 while (iter.hasNext())
3765 iter.nextFileInfo().dir().removeRecursively();
3766 }
3767
3768 int generatedComponents = 0;
3769 for (const auto &importPath : appImports) {
3770 ModuleInfo moduleInfo = getModuleInfo(importPath);
3771 if (!moduleInfo.isValid())
3772 continue;
3773
3774 const QString modulePackageSuffix = upperFirstAndAfterDot(moduleInfo.moduleName);
3775 if (moduleInfo.moduleName == libName) {
3776 fprintf(stderr,
3777 format: "A QML module name (%s) cannot be the same as the target name when building "
3778 "with QT_ANDROID_GENERATE_JAVA_QTQUICKVIEW_CONTENTS flag.\n",
3779 qPrintable(moduleInfo.moduleName));
3780 return false;
3781 }
3782
3783 const QString javaPackage = "%1.%2"_L1.arg(args&: javaPackageBase, args: modulePackageSuffix);
3784 const QString outputDir =
3785 "%1/%2"_L1.arg(args: baseSourceDir, args&: QString(modulePackageSuffix).replace(before: u'.', after: u'/'));
3786 if (!QDir().current().mkpath(dirPath: outputDir)) {
3787 fprintf(stderr, format: "Cannot create %s directory\n", qPrintable(outputDir));
3788 return false;
3789 }
3790
3791 // Add a marker file to indicate this as a module package source directory
3792 {
3793 QFile markerFile("%1/%2"_L1.arg(args: outputDir, args: markerFileName));
3794 if (!markerFile.open(flags: QFile::WriteOnly)) {
3795 fprintf(stderr, format: "Cannot create %s file\n", qPrintable(markerFile.fileName()));
3796 return false;
3797 }
3798 }
3799
3800 int indentBase = 0;
3801
3802 for (const auto &qmlComponent : moduleInfo.qmlComponents) {
3803 const bool isSelected = options.selectedJavaQmlComponents.contains(
3804 value: "%1.%2"_L1.arg(args&: moduleInfo.moduleName, args: qmlComponent.name));
3805 if (!options.selectedJavaQmlComponents.isEmpty() && !isSelected)
3806 continue;
3807
3808 QJsonObject domInfo = extractDomInfo(domBinaryPath, importPath, qmlComponent.path,
3809 externalImports + appImports);
3810 QJsonObject component = getComponent(domInfo);
3811 if (component.isEmpty())
3812 continue;
3813
3814 QByteArray componentClassBody;
3815 QTextStream outputStream(&componentClassBody, QTextStream::ReadWrite);
3816
3817 createHeaderBlock(outputStream, javaPackage);
3818
3819 beginComponentBlock(outputStream, libName, moduleInfo.moduleName, moduleInfo.preferPath,
3820 qmlComponent, indentBase);
3821 indentBase += 4;
3822
3823 const QJsonArray properties = getProperties(component);
3824 for (const QJsonValue &p : std::as_const(t: properties))
3825 beginPropertyBlock(outputStream, p.toObject(), indentBase);
3826
3827 const QJsonArray methods = getMethods(component);
3828 for (const QJsonValue &m : std::as_const(t: methods))
3829 beginSignalBlock(outputStream, m.toObject(), indentBase);
3830
3831 indentBase -= 4;
3832 endBlock(outputStream, indentBase);
3833 outputStream.flush();
3834
3835 // Write component class body to file
3836 QFile outputFile("%1/%2.java"_L1.arg(args: outputDir, args: qmlComponent.name));
3837 if (outputFile.exists())
3838 outputFile.remove();
3839 if (!outputFile.open(flags: QFile::WriteOnly)) {
3840 fprintf(stderr, format: "Cannot open %s file to write.\n",
3841 qPrintable(outputFile.fileName()));
3842 return false;
3843 }
3844 outputFile.write(data: componentClassBody);
3845 outputFile.close();
3846
3847 generatedComponents++;
3848 }
3849 }
3850 return generatedComponents;
3851}
3852
3853int main(int argc, char *argv[])
3854{
3855 QCoreApplication a(argc, argv);
3856
3857 Options options = parseOptions();
3858 if (options.helpRequested || options.outputDirectory.isEmpty()) {
3859 printHelp();
3860 return SyntaxErrorOrHelpRequested;
3861 }
3862
3863 options.timer.start();
3864
3865 if (!readInputFile(options: &options))
3866 return CannotReadInputFile;
3867
3868 if (Q_UNLIKELY(options.timing))
3869 fprintf(stdout, format: "[TIMING] %lld ns: Read input file\n", options.timer.nsecsElapsed());
3870
3871 fprintf(stdout,
3872 format: "Generating Android Package\n"
3873 " Input file: %s\n"
3874 " Output directory: %s\n"
3875 " Application binary: %s\n"
3876 " Android build platform: %s\n"
3877 " Install to device: %s\n",
3878 qPrintable(options.inputFileName),
3879 qPrintable(options.outputDirectory),
3880 qPrintable(options.applicationBinary),
3881 qPrintable(options.androidPlatform),
3882 options.installApk
3883 ? (options.installLocation.isEmpty() ? "Default device" : qPrintable(options.installLocation))
3884 : "No"
3885 );
3886
3887 bool androidTemplatetCopied = false;
3888
3889 for (auto it = options.architectures.constBegin(); it != options.architectures.constEnd(); ++it) {
3890 if (!it->enabled)
3891 continue;
3892 options.setCurrentQtArchitecture(arch: it.key(),
3893 directory: it.value().qtInstallDirectory,
3894 directories: it.value().qtDirectories);
3895
3896 // All architectures have a copy of the gradle files but only one set needs to be copied.
3897 if (!androidTemplatetCopied && options.build && !options.copyDependenciesOnly) {
3898 cleanAndroidFiles(options);
3899 if (Q_UNLIKELY(options.timing))
3900 fprintf(stdout, format: "[TIMING] %lld ns: Cleaned Android file\n", options.timer.nsecsElapsed());
3901
3902 if (!copyAndroidTemplate(options))
3903 return CannotCopyAndroidTemplate;
3904
3905 if (Q_UNLIKELY(options.timing))
3906 fprintf(stdout, format: "[TIMING] %lld ns: Copied Android template\n", options.timer.nsecsElapsed());
3907 androidTemplatetCopied = true;
3908 }
3909
3910 if (!readDependencies(options: &options))
3911 return CannotReadDependencies;
3912
3913 if (Q_UNLIKELY(options.timing))
3914 fprintf(stdout, format: "[TIMING] %lld ns: Read dependencies\n", options.timer.nsecsElapsed());
3915
3916 if (!copyQtFiles(options: &options))
3917 return CannotCopyQtFiles;
3918
3919 if (Q_UNLIKELY(options.timing))
3920 fprintf(stdout, format: "[TIMING] %lld ns: Copied Qt files\n", options.timer.nsecsElapsed());
3921
3922 if (!copyAndroidExtraLibs(options: &options))
3923 return CannotCopyAndroidExtraLibs;
3924
3925 if (Q_UNLIKELY(options.timing))
3926 fprintf(stdout, format: "[TIMING] %lld ms: Copied extra libs\n", options.timer.nsecsElapsed());
3927
3928 if (!copyAndroidExtraResources(options: &options))
3929 return CannotCopyAndroidExtraResources;
3930
3931 if (Q_UNLIKELY(options.timing))
3932 fprintf(stdout, format: "[TIMING] %lld ns: Copied extra resources\n", options.timer.nsecsElapsed());
3933
3934 if (!copyStdCpp(options: &options))
3935 return CannotCopyGnuStl;
3936
3937 if (Q_UNLIKELY(options.timing))
3938 fprintf(stdout, format: "[TIMING] %lld ns: Copied GNU STL\n", options.timer.nsecsElapsed());
3939
3940 if (options.generateJavaQmlComponents) {
3941 if (!generateJavaQmlComponents(options))
3942 return CannotGenerateJavaQmlComponents;
3943 }
3944
3945 if (Q_UNLIKELY(options.timing)) {
3946 fprintf(stdout, format: "[TIMING] %lld ns: Generate Java QtQuickViewContents.\n",
3947 options.timer.nsecsElapsed());
3948 }
3949
3950 // If Unbundled deployment is used, remove app lib as we don't want it packaged inside the APK
3951 if (options.deploymentMechanism == Options::Unbundled) {
3952 QString appLibPath = "%1/libs/%2/lib%3_%2.so"_L1.
3953 arg(args&: options.outputDirectory,
3954 args&: options.currentArchitecture,
3955 args&: options.applicationBinary);
3956 QFile::remove(fileName: appLibPath);
3957 } else if (!containsApplicationBinary(options: &options)) {
3958 return CannotFindApplicationBinary;
3959 }
3960
3961 if (Q_UNLIKELY(options.timing))
3962 fprintf(stdout, format: "[TIMING] %lld ns: Checked for application binary\n", options.timer.nsecsElapsed());
3963
3964 if (Q_UNLIKELY(options.timing))
3965 fprintf(stdout, format: "[TIMING] %lld ns: Bundled Qt libs\n", options.timer.nsecsElapsed());
3966 }
3967
3968 if (options.copyDependenciesOnly) {
3969 if (!options.depFilePath.isEmpty())
3970 writeDependencyFile(options);
3971 return 0;
3972 }
3973
3974 if (!createRcc(options))
3975 return CannotCreateRcc;
3976
3977 if (options.auxMode) {
3978 if (!updateAndroidFiles(options))
3979 return CannotUpdateAndroidFiles;
3980 return 0;
3981 }
3982
3983 if (options.build) {
3984 if (!copyAndroidSources(options))
3985 return CannotCopyAndroidSources;
3986
3987 if (Q_UNLIKELY(options.timing))
3988 fprintf(stdout, format: "[TIMING] %lld ns: Copied android sources\n", options.timer.nsecsElapsed());
3989
3990 if (!updateAndroidFiles(options))
3991 return CannotUpdateAndroidFiles;
3992
3993 if (Q_UNLIKELY(options.timing))
3994 fprintf(stdout, format: "[TIMING] %lld ns: Updated files\n", options.timer.nsecsElapsed());
3995
3996 if (Q_UNLIKELY(options.timing))
3997 fprintf(stdout, format: "[TIMING] %lld ns: Created project\n", options.timer.nsecsElapsed());
3998
3999 if (!buildAndroidProject(options))
4000 return CannotBuildAndroidProject;
4001
4002 if (Q_UNLIKELY(options.timing))
4003 fprintf(stdout, format: "[TIMING] %lld ns: Built project\n", options.timer.nsecsElapsed());
4004
4005 if (!options.keyStore.isEmpty() && !signPackage(options))
4006 return CannotSignPackage;
4007
4008 if (!options.apkPath.isEmpty() && !copyPackage(options))
4009 return CannotCopyApk;
4010
4011 if (Q_UNLIKELY(options.timing))
4012 fprintf(stdout, format: "[TIMING] %lld ns: Signed package\n", options.timer.nsecsElapsed());
4013 }
4014
4015 if (options.installApk && !installApk(options))
4016 return CannotInstallApk;
4017
4018 if (Q_UNLIKELY(options.timing))
4019 fprintf(stdout, format: "[TIMING] %lld ns: Installed APK\n", options.timer.nsecsElapsed());
4020
4021 if (!options.depFilePath.isEmpty())
4022 writeDependencyFile(options);
4023
4024 fprintf(stdout, format: "Android package built successfully in %.3f ms.\n", options.timer.elapsed() / 1000.);
4025
4026 if (options.installApk)
4027 fprintf(stdout, format: " -- It can now be run from the selected device/emulator.\n");
4028
4029 fprintf(stdout, format: " -- File: %s\n", qPrintable(packagePath(options, options.keyStore.isEmpty() ? UnsignedAPK
4030 : SignedAPK)));
4031 fflush(stdout);
4032 return 0;
4033}
4034

source code of qtbase/src/tools/androiddeployqt/main.cpp