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

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