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