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

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