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