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