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

Provided by KDAB

Privacy Policy
Learn to use CMake with our Intro Training
Find out more

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