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