summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJan Arve Sæther <jan-arve.saether@qt.io>2018-06-13 15:52:59 +0200
committerJan Arve Sæther <jan-arve.saether@qt.io>2018-06-21 11:08:57 +0000
commit6af262b6d76ba5dc6d1fa7d7abea460039fea6b9 (patch)
treed685919fea3780300b47533e7bbc3d452c84fadb
parent9f8938b89a67ff8da148b7b865576a3d5584c610 (diff)
Move androiddeployqt to qtbase
androiddeploytqt is the only official way of deploying applications to the device, so it is therefore part of the "platform". It therefore needs to live in qtbase. Change-Id: I52d7c4427275aacec792b71284a0c10edaf7ab69 Reviewed-by: Frederik Gladhorn <frederik.gladhorn@qt.io>
-rw-r--r--src/src.pro10
-rw-r--r--src/tools/androiddeployqt/androiddeployqt.pro14
-rw-r--r--src/tools/androiddeployqt/main.cpp2909
3 files changed, 2933 insertions, 0 deletions
diff --git a/src/src.pro b/src/src.pro
index 1f7c5d99c1..1c76a2e46f 100644
--- a/src/src.pro
+++ b/src/src.pro
@@ -53,6 +53,10 @@ src_tools_qdbuscpp2xml.target = sub-qdbuscpp2xml
force_bootstrap: src_tools_qdbuscpp2xml.depends = src_tools_bootstrap_dbus
else: src_tools_qdbuscpp2xml.depends = src_dbus
+src_tools_androiddeployqt.subdir = tools/androiddeployqt
+src_tools_androiddeployqt.target = sub-androiddeployqt
+src_tools_androiddeployqt.depends = src_corelib
+
src_tools_qvkgen.subdir = tools/qvkgen
src_tools_qvkgen.target = sub-qvkgen
force_bootstrap: src_tools_qvkgen.depends = src_tools_bootstrap
@@ -183,6 +187,12 @@ qtConfig(dbus) {
src_platformsupport.depends += src_dbus src_tools_qdbusxml2cpp
src_plugins.depends += src_dbus src_tools_qdbusxml2cpp src_tools_qdbuscpp2xml
}
+
+android {
+ SUBDIRS += src_tools_androiddeployqt
+ TOOLS += src_tools_androiddeployqt
+}
+
qtConfig(concurrent): SUBDIRS += src_concurrent
qtConfig(gui) {
qtConfig(harfbuzz):!qtConfig(system-harfbuzz) {
diff --git a/src/tools/androiddeployqt/androiddeployqt.pro b/src/tools/androiddeployqt/androiddeployqt.pro
new file mode 100644
index 0000000000..2d0f5b41d1
--- /dev/null
+++ b/src/tools/androiddeployqt/androiddeployqt.pro
@@ -0,0 +1,14 @@
+option(host_build)
+CONFIG += console
+
+SOURCES += \
+ main.cpp
+
+# Required for declarations of popen/pclose on Windows
+windows: QMAKE_CXXFLAGS += -U__STRICT_ANSI__
+
+DEFINES += QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII
+DEFINES += QT_NO_FOREACH
+
+load(qt_app)
+
diff --git a/src/tools/androiddeployqt/main.cpp b/src/tools/androiddeployqt/main.cpp
new file mode 100644
index 0000000000..f00188396d
--- /dev/null
+++ b/src/tools/androiddeployqt/main.cpp
@@ -0,0 +1,2909 @@
+/****************************************************************************
+**
+** Copyright (C) 2016 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the tools applications of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:GPL-EXCEPT$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QCoreApplication>
+#include <QStringList>
+#include <QDir>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QJsonValue>
+#include <QDebug>
+#include <QDataStream>
+#include <QXmlStreamReader>
+#include <QDateTime>
+#include <QStandardPaths>
+#include <QUuid>
+#include <QDirIterator>
+#include <QRegExp>
+
+#include <algorithm>
+static const bool mustReadOutputAnyway = true; // pclose seems to return the wrong error code unless we read the output
+
+void deleteRecursively(const QString &dirName)
+{
+ QDir dir(dirName);
+ if (!dir.exists())
+ return;
+
+ const QFileInfoList entries = dir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs);
+ for (const QFileInfo &entry : entries) {
+ if (entry.isDir())
+ deleteRecursively(entry.absoluteFilePath());
+ else
+ QFile::remove(entry.absoluteFilePath());
+ }
+
+ QDir().rmdir(dirName);
+}
+
+FILE *openProcess(const QString &command)
+{
+#if defined(Q_OS_WIN32)
+ QString processedCommand = QLatin1Char('\"') + command + QLatin1Char('\"');
+#else
+ QString processedCommand = command;
+#endif
+
+ return popen(processedCommand.toLocal8Bit().constData(), "r");
+}
+
+struct QtDependency
+{
+ QtDependency(QString rpath, QString apath) : relativePath(rpath), absolutePath(apath) {}
+
+ bool operator==(const QtDependency &other) const
+ {
+ return relativePath == other.relativePath && absolutePath == other.absolutePath;
+ }
+
+ QString relativePath;
+ QString absolutePath;
+};
+
+struct Options
+{
+ Options()
+ : helpRequested(false)
+ , verbose(false)
+ , timing(false)
+ , generateAssetsFileList(true)
+ , build(true)
+ , gradle(false)
+ , deploymentMechanism(Bundled)
+ , releasePackage(false)
+ , digestAlg(QLatin1String("SHA1"))
+ , sigAlg(QLatin1String("SHA1withRSA"))
+ , internalSf(false)
+ , sectionsOnly(false)
+ , protectedAuthenticationPath(false)
+ , jarSigner(false)
+ , gdbServer(Auto)
+ , installApk(false)
+ , uninstallApk(false)
+ {}
+
+ enum DeploymentMechanism
+ {
+ Bundled,
+ Ministro
+ };
+
+ enum TriState {
+ Auto,
+ False,
+ True
+ };
+
+ bool helpRequested;
+ bool verbose;
+ bool timing;
+ bool generateAssetsFileList;
+ bool build;
+ bool gradle;
+ QTime timer;
+
+ // External tools
+ QString sdkPath;
+ QString sdkBuildToolsVersion;
+ QString ndkPath;
+ QString antTool;
+ QString jdkPath;
+
+ // Build paths
+ QString qtInstallDirectory;
+ QString androidSourceDirectory;
+ QString outputDirectory;
+ QString inputFileName;
+ QString applicationBinary;
+ QString rootPath;
+ QStringList qmlImportPaths;
+
+ // lib c++ path
+ QString stdCppPath;
+ QString stdCppName = QStringLiteral("gnustl_shared");
+
+ // Build information
+ QString androidPlatform;
+ QString architecture;
+ QString toolchainVersion;
+ QString toolchainPrefix;
+ QString toolPrefix;
+ QString ndkHost;
+
+ // Package information
+ DeploymentMechanism deploymentMechanism;
+ QString packageName;
+ QStringList extraLibs;
+ QStringList extraPlugins;
+
+ // Signing information
+ bool releasePackage;
+ QString keyStore;
+ QString keyStorePassword;
+ QString keyStoreAlias;
+ QString storeType;
+ QString keyPass;
+ QString sigFile;
+ QString signedJar;
+ QString digestAlg;
+ QString sigAlg;
+ QString tsaUrl;
+ QString tsaCert;
+ bool internalSf;
+ bool sectionsOnly;
+ bool protectedAuthenticationPath;
+ bool jarSigner;
+
+ // Gdbserver
+ TriState gdbServer;
+
+ // Installation information
+ bool installApk;
+ bool uninstallApk;
+ QString installLocation;
+
+ // Collected information
+ typedef QPair<QString, QString> BundledFile;
+ QList<BundledFile> bundledFiles;
+ QList<QtDependency> qtDependencies;
+ QStringList localLibs;
+ QStringList localJars;
+ QStringList initClasses;
+ QStringList permissions;
+ QStringList features;
+};
+
+// Copy-pasted from qmake/library/ioutil.cpp
+inline static bool hasSpecialChars(const QString &arg, const uchar (&iqm)[16])
+{
+ for (int x = arg.length() - 1; x >= 0; --x) {
+ ushort c = arg.unicode()[x].unicode();
+ if ((c < sizeof(iqm) * 8) && (iqm[c / 8] & (1 << (c & 7))))
+ return true;
+ }
+ return false;
+}
+
+static QString shellQuoteUnix(const QString &arg)
+{
+ // Chars that should be quoted (TM). This includes:
+ static const uchar iqm[] = {
+ 0xff, 0xff, 0xff, 0xff, 0xdf, 0x07, 0x00, 0xd8,
+ 0x00, 0x00, 0x00, 0x38, 0x01, 0x00, 0x00, 0x78
+ }; // 0-32 \'"$`<>|;&(){}*?#!~[]
+
+ if (!arg.length())
+ return QString::fromLatin1("\"\"");
+
+ QString ret(arg);
+ if (hasSpecialChars(ret, iqm)) {
+ ret.replace(QLatin1Char('\''), QLatin1String("'\\''"));
+ ret.prepend(QLatin1Char('\''));
+ ret.append(QLatin1Char('\''));
+ }
+ return ret;
+}
+
+static QString shellQuoteWin(const QString &arg)
+{
+ // Chars that should be quoted (TM). This includes:
+ // - control chars & space
+ // - the shell meta chars "&()<>^|
+ // - the potential separators ,;=
+ static const uchar iqm[] = {
+ 0xff, 0xff, 0xff, 0xff, 0x45, 0x13, 0x00, 0x78,
+ 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x10
+ };
+
+ if (!arg.length())
+ return QString::fromLatin1("\"\"");
+
+ QString ret(arg);
+ if (hasSpecialChars(ret, iqm)) {
+ // Quotes are escaped and their preceding backslashes are doubled.
+ // It's impossible to escape anything inside a quoted string on cmd
+ // level, so the outer quoting must be "suspended".
+ ret.replace(QRegExp(QLatin1String("(\\\\*)\"")), QLatin1String("\"\\1\\1\\^\"\""));
+ // The argument must not end with a \ since this would be interpreted
+ // as escaping the quote -- rather put the \ behind the quote: e.g.
+ // rather use "foo"\ than "foo\"
+ int i = ret.length();
+ while (i > 0 && ret.at(i - 1) == QLatin1Char('\\'))
+ --i;
+ ret.insert(i, QLatin1Char('"'));
+ ret.prepend(QLatin1Char('"'));
+ }
+ return ret;
+}
+
+static QString shellQuote(const QString &arg)
+{
+ if (QDir::separator() == QLatin1Char('\\'))
+ return shellQuoteWin(arg);
+ else
+ return shellQuoteUnix(arg);
+}
+
+
+void deleteMissingFiles(const Options &options, const QDir &srcDir, const QDir &dstDir)
+{
+ if (options.verbose)
+ fprintf(stdout, "Delete missing files %s %s\n", qPrintable(srcDir.absolutePath()), qPrintable(dstDir.absolutePath()));
+
+ const QFileInfoList srcEntries = srcDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs);
+ const QFileInfoList dstEntries = dstDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs);
+ for (const QFileInfo &dst : dstEntries) {
+ bool found = false;
+ for (const QFileInfo &src : srcEntries)
+ if (dst.fileName() == src.fileName()) {
+ if (dst.isDir())
+ deleteMissingFiles(options, src.absoluteFilePath(), dst.absoluteFilePath());
+ found = true;
+ break;
+ }
+
+ if (!found) {
+ if (options.verbose)
+ fprintf(stdout, "%s not found in %s, removing it.\n", qPrintable(dst.fileName()), qPrintable(srcDir.absolutePath()));
+
+ if (dst.isDir())
+ deleteRecursively(dst.absolutePath());
+ else
+ QFile::remove(dst.absoluteFilePath());
+ }
+ }
+ fflush(stdout);
+}
+
+
+Options parseOptions()
+{
+ Options options;
+
+ QStringList arguments = QCoreApplication::arguments();
+ for (int i=0; i<arguments.size(); ++i) {
+ const QString &argument = arguments.at(i);
+ if (argument.compare(QLatin1String("--output"), Qt::CaseInsensitive) == 0) {
+ if (i + 1 == arguments.size())
+ options.helpRequested = true;
+ else
+ options.outputDirectory = arguments.at(++i).trimmed();
+ } else if (argument.compare(QLatin1String("--input"), Qt::CaseInsensitive) == 0) {
+ if (i + 1 == arguments.size())
+ options.helpRequested = true;
+ else
+ options.inputFileName = arguments.at(++i);
+ } else if (argument.compare(QLatin1String("--no-build"), Qt::CaseInsensitive) == 0) {
+ options.build = false;
+ } else if (argument.compare(QLatin1String("--install"), Qt::CaseInsensitive) == 0) {
+ options.installApk = true;
+ options.uninstallApk = true;
+ } else if (argument.compare(QLatin1String("--reinstall"), Qt::CaseInsensitive) == 0) {
+ options.installApk = true;
+ options.uninstallApk = false;
+ } else if (argument.compare(QLatin1String("--android-platform"), Qt::CaseInsensitive) == 0) {
+ if (i + 1 == arguments.size())
+ options.helpRequested = true;
+ else
+ options.androidPlatform = arguments.at(++i);
+ } else if (argument.compare(QLatin1String("--help"), Qt::CaseInsensitive) == 0) {
+ options.helpRequested = true;
+ } else if (argument.compare(QLatin1String("--verbose"), Qt::CaseInsensitive) == 0) {
+ options.verbose = true;
+ } else if (argument.compare(QLatin1String("--gradle"), Qt::CaseInsensitive) == 0) {
+ options.gradle = true;
+ } else if (argument.compare(QLatin1String("--ant"), Qt::CaseInsensitive) == 0) {
+ if (i + 1 == arguments.size())
+ options.helpRequested = true;
+ else
+ options.antTool = arguments.at(++i);
+ } else if (argument.compare(QLatin1String("--deployment"), Qt::CaseInsensitive) == 0) {
+ if (i + 1 == arguments.size()) {
+ options.helpRequested = true;
+ } else {
+ QString deploymentMechanism = arguments.at(++i);
+ if (deploymentMechanism.compare(QLatin1String("ministro"), Qt::CaseInsensitive) == 0) {
+ options.deploymentMechanism = Options::Ministro;
+ } else if (deploymentMechanism.compare(QLatin1String("bundled"), Qt::CaseInsensitive) == 0) {
+ options.deploymentMechanism = Options::Bundled;
+ } else {
+ fprintf(stderr, "Unrecognized deployment mechanism: %s\n", qPrintable(deploymentMechanism));
+ options.helpRequested = true;
+ }
+ }
+ } else if (argument.compare(QLatin1String("--device"), Qt::CaseInsensitive) == 0) {
+ if (i + 1 == arguments.size())
+ options.helpRequested = true;
+ else
+ options.installLocation = arguments.at(++i);
+ } else if (argument.compare(QLatin1String("--release"), Qt::CaseInsensitive) == 0) {
+ options.releasePackage = true;
+ } else if (argument.compare(QLatin1String("--gdbserver"), Qt::CaseInsensitive) == 0) {
+ options.gdbServer = Options::True;
+ } else if (argument.compare(QLatin1String("--no-gdbserver"), Qt::CaseInsensitive) == 0) {
+ options.gdbServer = Options::False;
+ } else if (argument.compare(QLatin1String("--jdk"), Qt::CaseInsensitive) == 0) {
+ if (i + 1 == arguments.size())
+ options.helpRequested = true;
+ else
+ options.jdkPath = arguments.at(++i);
+ } else if (argument.compare(QLatin1String("--sign"), Qt::CaseInsensitive) == 0) {
+ if (i + 2 >= arguments.size()) {
+ options.helpRequested = true;
+ } else {
+ options.releasePackage = true;
+ options.keyStore = arguments.at(++i);
+ options.keyStoreAlias = arguments.at(++i);
+ }
+ } else if (argument.compare(QLatin1String("--storepass"), Qt::CaseInsensitive) == 0) {
+ if (i + 1 == arguments.size())
+ options.helpRequested = true;
+ else
+ options.keyStorePassword = arguments.at(++i);
+ } else if (argument.compare(QLatin1String("--storetype"), Qt::CaseInsensitive) == 0) {
+ if (i + 1 == arguments.size())
+ options.helpRequested = true;
+ else
+ options.storeType = arguments.at(++i);
+ } else if (argument.compare(QLatin1String("--keypass"), Qt::CaseInsensitive) == 0) {
+ if (i + 1 == arguments.size())
+ options.helpRequested = true;
+ else
+ options.keyPass = arguments.at(++i);
+ } else if (argument.compare(QLatin1String("--sigfile"), Qt::CaseInsensitive) == 0) {
+ if (i + 1 == arguments.size())
+ options.helpRequested = true;
+ else
+ options.sigFile = arguments.at(++i);
+ } else if (argument.compare(QLatin1String("--digestalg"), Qt::CaseInsensitive) == 0) {
+ if (i + 1 == arguments.size())
+ options.helpRequested = true;
+ else
+ options.digestAlg = arguments.at(++i);
+ } else if (argument.compare(QLatin1String("--sigalg"), Qt::CaseInsensitive) == 0) {
+ if (i + 1 == arguments.size())
+ options.helpRequested = true;
+ else
+ options.sigAlg = arguments.at(++i);
+ } else if (argument.compare(QLatin1String("--tsa"), Qt::CaseInsensitive) == 0) {
+ if (i + 1 == arguments.size())
+ options.helpRequested = true;
+ else
+ options.tsaUrl = arguments.at(++i);
+ } else if (argument.compare(QLatin1String("--tsacert"), Qt::CaseInsensitive) == 0) {
+ if (i + 1 == arguments.size())
+ options.helpRequested = true;
+ else
+ options.tsaCert = arguments.at(++i);
+ } else if (argument.compare(QLatin1String("--internalsf"), Qt::CaseInsensitive) == 0) {
+ options.internalSf = true;
+ } else if (argument.compare(QLatin1String("--sectionsonly"), Qt::CaseInsensitive) == 0) {
+ options.sectionsOnly = true;
+ } else if (argument.compare(QLatin1String("--protected"), Qt::CaseInsensitive) == 0) {
+ options.protectedAuthenticationPath = true;
+ } else if (argument.compare(QLatin1String("--jarsigner"), Qt::CaseInsensitive) == 0) {
+ options.jarSigner = true;
+ } else if (argument.compare(QLatin1String("--no-generated-assets-cache"), Qt::CaseInsensitive) == 0) {
+ options.generateAssetsFileList = false;
+ }
+ }
+
+ if (options.inputFileName.isEmpty())
+ options.inputFileName = QString::fromLatin1("android-lib%1.so-deployment-settings.json").arg(QDir::current().dirName());
+
+ options.timing = qEnvironmentVariableIsSet("ANDROIDDEPLOYQT_TIMING_OUTPUT");
+
+ if (!QDir::current().mkpath(options.outputDirectory)) {
+ fprintf(stderr, "Invalid output directory: %s\n", qPrintable(options.outputDirectory));
+ options.outputDirectory.clear();
+ } else {
+ options.outputDirectory = QFileInfo(options.outputDirectory).canonicalFilePath();
+ if (!options.outputDirectory.endsWith(QLatin1Char('/')))
+ options.outputDirectory += QLatin1Char('/');
+ }
+
+ return options;
+}
+
+void printHelp()
+{// "012345678901234567890123456789012345678901234567890123456789012345678901"
+ fprintf(stderr, "Syntax: %s --output <destination> [options]\n"
+ "\n"
+ " Creates an Android package in the build directory <destination> and\n"
+ " builds it into an .apk file.\n\n"
+ " Optional arguments:\n"
+ " --input <inputfile>: Reads <inputfile> for options generated by\n"
+ " qmake. A default file name based on the current working\n"
+ " directory will be used if nothing else is specified.\n"
+ " --deployment <mechanism>: Supported deployment mechanisms:\n"
+ " bundled (default): Include Qt files in stand-alone package.\n"
+ " ministro: Use the Ministro service to manage Qt files.\n"
+ " --no-build: Do not build the package, it is useful to just install\n"
+ " a package previously built.\n"
+ " --install: Installs apk to device/emulator. By default this step is\n"
+ " not taken. If the application has previously been installed on\n"
+ " the device, it will be uninstalled first.\n"
+ " --reinstall: Installs apk to device/emulator. By default this step\n"
+ " is not taken. If the application has previously been installed on\n"
+ " the device, it will be overwritten, but its data will be left\n"
+ " intact.\n"
+ " --device [device ID]: Use specified device for deployment. Default\n"
+ " is the device selected by default by adb.\n"
+ " --android-platform <platform>: Builds against the given android\n"
+ " platform. By default, the highest available version will be\n"
+ " used.\n"
+ " --gradle. Use gradle instead of ant to create and install the apk.\n"
+ " --ant <path/to/ant>: If unspecified, ant from the PATH will be\n"
+ " used.\n"
+ " --release: Builds a package ready for release. By default, the\n"
+ " package will be signed with a debug key.\n"
+ " --sign <url/to/keystore> <alias>: Signs the package with the\n"
+ " specified keystore, alias and store password. Also implies the\n"
+ " --release option.\n"
+ " Optional arguments for use with signing:\n"
+ " --storepass <password>: Keystore password.\n"
+ " --storetype <type>: Keystore type.\n"
+ " --keypass <password>: Password for private key (if different\n"
+ " from keystore password.)\n"
+ " --sigfile <file>: Name of .SF/.DSA file.\n"
+ " --digestalg <name>: Name of digest algorithm. Default is\n"
+ " \"SHA1\".\n"
+ " --sigalg <name>: Name of signature algorithm. Default is\n"
+ " \"SHA1withRSA\".\n"
+ " --tsa <url>: Location of the Time Stamping Authority.\n"
+ " --tsacert <alias>: Public key certificate for TSA.\n"
+ " --internalsf: Include the .SF file inside the signature block.\n"
+ " --sectionsonly: Don't compute hash of entire manifest.\n"
+ " --protected: Keystore has protected authentication path.\n"
+ " --jarsigner: Force jarsigner usage, otherwise apksigner will be\n"
+ " used if available.\n"
+ " --gdbserver: Adds the gdbserver to the package. By default the gdbserver\n"
+ " is bundled for debug pacakges.\n"
+ " --no-gdbserver: Prevents the gdbserver from being added to the package\n"
+ " By default the gdbserver is bundled for debug pacakges.\n"
+ " --jdk <path/to/jdk>: Used to find the jarsigner tool when used\n"
+ " in combination with the --release argument. By default,\n"
+ " an attempt is made to detect the tool using the JAVA_HOME and\n"
+ " PATH environment variables, in that order.\n"
+ " --qml-import-paths: Specify additional search paths for QML\n"
+ " imports.\n"
+ " --verbose: Prints out information during processing.\n"
+ " --no-generated-assets-cache: Do not pregenerate the entry list for\n"
+ " the assets file engine.\n"
+ " --help: Displays this information.\n\n",
+ qPrintable(QCoreApplication::arguments().at(0))
+ );
+}
+
+// Since strings compared will all start with the same letters,
+// sorting by length and then alphabetically within each length
+// gives the natural order.
+bool quasiLexicographicalReverseLessThan(const QFileInfo &fi1, const QFileInfo &fi2)
+{
+ QString s1 = fi1.baseName();
+ QString s2 = fi2.baseName();
+
+ if (s1.length() == s2.length())
+ return s1 > s2;
+ else
+ return s1.length() > s2.length();
+}
+
+// Files which contain templates that need to be overwritten by build data should be overwritten every
+// time.
+bool alwaysOverwritableFile(const QString &fileName)
+{
+ return (fileName.endsWith(QLatin1String("/res/values/libs.xml"))
+ || fileName.endsWith(QLatin1String("/AndroidManifest.xml"))
+ || fileName.endsWith(QLatin1String("/res/values/strings.xml"))
+ || fileName.endsWith(QLatin1String("/src/org/qtproject/qt5/android/bindings/QtActivity.java")));
+}
+
+bool copyFileIfNewer(const QString &sourceFileName,
+ const QString &destinationFileName,
+ bool verbose,
+ bool forceOverwrite = false)
+{
+ if (QFile::exists(destinationFileName)) {
+ QFileInfo destinationFileInfo(destinationFileName);
+ QFileInfo sourceFileInfo(sourceFileName);
+
+ if (!forceOverwrite
+ && sourceFileInfo.lastModified() <= destinationFileInfo.lastModified()
+ && !alwaysOverwritableFile(destinationFileName)) {
+ if (verbose)
+ fprintf(stdout, " -- Skipping file %s. Same or newer file already in place.\n", qPrintable(sourceFileName));
+ return true;
+ } else {
+ if (!QFile(destinationFileName).remove()) {
+ fprintf(stderr, "Can't remove old file: %s\n", qPrintable(destinationFileName));
+ return false;
+ }
+ }
+ }
+
+ if (!QDir().mkpath(QFileInfo(destinationFileName).path())) {
+ fprintf(stderr, "Cannot make output directory for %s.\n", qPrintable(destinationFileName));
+ return false;
+ }
+
+ if (!QFile::exists(destinationFileName) && !QFile::copy(sourceFileName, destinationFileName)) {
+ fprintf(stderr, "Failed to copy %s to %s.\n", qPrintable(sourceFileName), qPrintable(destinationFileName));
+ return false;
+ } else if (verbose) {
+ fprintf(stdout, " -- Copied %s\n", qPrintable(destinationFileName));
+ fflush(stdout);
+ }
+
+ return true;
+}
+
+QString cleanPackageName(QString packageName)
+{
+ QRegExp legalChars(QLatin1String("[a-zA-Z0-9_\\.]"));
+
+ for (int i = 0; i < packageName.length(); ++i) {
+ if (!legalChars.exactMatch(packageName.mid(i, 1)))
+ packageName[i] = QLatin1Char('_');
+ }
+
+ static QStringList keywords;
+ if (keywords.isEmpty()) {
+ keywords << QLatin1String("abstract") << QLatin1String("continue") << QLatin1String("for")
+ << QLatin1String("new") << QLatin1String("switch") << QLatin1String("assert")
+ << QLatin1String("default") << QLatin1String("if") << QLatin1String("package")
+ << QLatin1String("synchronized") << QLatin1String("boolean") << QLatin1String("do")
+ << QLatin1String("goto") << QLatin1String("private") << QLatin1String("this")
+ << QLatin1String("break") << QLatin1String("double") << QLatin1String("implements")
+ << QLatin1String("protected") << QLatin1String("throw") << QLatin1String("byte")
+ << QLatin1String("else") << QLatin1String("import") << QLatin1String("public")
+ << QLatin1String("throws") << QLatin1String("case") << QLatin1String("enum")
+ << QLatin1String("instanceof") << QLatin1String("return") << QLatin1String("transient")
+ << QLatin1String("catch") << QLatin1String("extends") << QLatin1String("int")
+ << QLatin1String("short") << QLatin1String("try") << QLatin1String("char")
+ << QLatin1String("final") << QLatin1String("interface") << QLatin1String("static")
+ << QLatin1String("void") << QLatin1String("class") << QLatin1String("finally")
+ << QLatin1String("long") << QLatin1String("strictfp") << QLatin1String("volatile")
+ << QLatin1String("const") << QLatin1String("float") << QLatin1String("native")
+ << QLatin1String("super") << QLatin1String("while");
+ }
+
+ // No keywords
+ int index = -1;
+ while (index < packageName.length()) {
+ int next = packageName.indexOf(QLatin1Char('.'), index + 1);
+ if (next == -1)
+ next = packageName.length();
+ QString word = packageName.mid(index + 1, next - index - 1);
+ if (!word.isEmpty()) {
+ QChar c = word[0];
+ if ((c >= QChar(QLatin1Char('0')) && c<= QChar(QLatin1Char('9')))
+ || c == QLatin1Char('_')) {
+ packageName.insert(index + 1, QLatin1Char('a'));
+ index = next + 1;
+ continue;
+ }
+ }
+ if (keywords.contains(word)) {
+ packageName.insert(next, QLatin1String("_"));
+ index = next + 1;
+ } else {
+ index = next;
+ }
+ }
+
+ return packageName;
+}
+
+QString detectLatestAndroidPlatform(const QString &sdkPath)
+{
+ QDir dir(sdkPath + QLatin1String("/platforms"));
+ if (!dir.exists()) {
+ fprintf(stderr, "Directory %s does not exist\n", qPrintable(dir.absolutePath()));
+ return QString();
+ }
+
+ QFileInfoList fileInfos = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
+ if (fileInfos.isEmpty()) {
+ fprintf(stderr, "No platforms found in %s", qPrintable(dir.absolutePath()));
+ return QString();
+ }
+
+ std::sort(fileInfos.begin(), fileInfos.end(), quasiLexicographicalReverseLessThan);
+
+ QFileInfo latestPlatform = fileInfos.first();
+ return latestPlatform.baseName();
+}
+
+QString packageNameFromAndroidManifest(const QString &androidManifestPath)
+{
+ QFile androidManifestXml(androidManifestPath);
+ if (androidManifestXml.open(QIODevice::ReadOnly)) {
+ QXmlStreamReader reader(&androidManifestXml);
+ while (!reader.atEnd()) {
+ reader.readNext();
+ if (reader.isStartElement() && reader.name() == QLatin1String("manifest"))
+ return cleanPackageName(
+ reader.attributes().value(QLatin1String("package")).toString());
+ }
+ }
+ return QString();
+}
+
+bool readInputFile(Options *options)
+{
+ QFile file(options->inputFileName);
+ if (!file.open(QIODevice::ReadOnly)) {
+ fprintf(stderr, "Cannot read from input file: %s\n", qPrintable(options->inputFileName));
+ return false;
+ }
+
+ QJsonDocument jsonDocument = QJsonDocument::fromJson(file.readAll());
+ if (jsonDocument.isNull()) {
+ fprintf(stderr, "Invalid json file: %s\n", qPrintable(options->inputFileName));
+ return false;
+ }
+
+ QJsonObject jsonObject = jsonDocument.object();
+
+ {
+ QJsonValue sdkPath = jsonObject.value(QLatin1String("sdk"));
+ if (sdkPath.isUndefined()) {
+ fprintf(stderr, "No SDK path in json file %s\n", qPrintable(options->inputFileName));
+ return false;
+ }
+
+ options->sdkPath = sdkPath.toString();
+
+ if (options->androidPlatform.isEmpty()) {
+ options->androidPlatform = detectLatestAndroidPlatform(options->sdkPath);
+ if (options->androidPlatform.isEmpty())
+ return false;
+ } else {
+ if (!QDir(options->sdkPath + QLatin1String("/platforms/") + options->androidPlatform).exists()) {
+ fprintf(stderr, "Warning: Android platform '%s' does not exist in SDK.\n",
+ qPrintable(options->androidPlatform));
+ }
+ }
+ }
+
+ {
+
+ const QJsonValue value = jsonObject.value(QStringLiteral("sdkBuildToolsRevision"));
+ if (!value.isUndefined())
+ options->sdkBuildToolsVersion = value.toString();
+ }
+
+ {
+ const QJsonValue qtInstallDirectory = jsonObject.value(QStringLiteral("qt"));
+ if (qtInstallDirectory.isUndefined()) {
+ fprintf(stderr, "No Qt directory in json file %s\n", qPrintable(options->inputFileName));
+ return false;
+ }
+ options->qtInstallDirectory = qtInstallDirectory.toString();
+ }
+
+ {
+ const QJsonValue androidSourcesDirectory = jsonObject.value(QStringLiteral("android-package-source-directory"));
+ if (!androidSourcesDirectory.isUndefined())
+ options->androidSourceDirectory = androidSourcesDirectory.toString();
+ }
+
+ {
+ const QJsonValue applicationBinary = jsonObject.value(QStringLiteral("application-binary"));
+ if (applicationBinary.isUndefined()) {
+ fprintf(stderr, "No application binary defined in json file.\n");
+ return false;
+ }
+ options->applicationBinary = applicationBinary.toString();
+
+ if (!QFile::exists(options->applicationBinary)) {
+ fprintf(stderr, "Cannot find application binary %s.\n", qPrintable(options->applicationBinary));
+ return false;
+ }
+ }
+
+ {
+ const QJsonValue deploymentDependencies = jsonObject.value(QStringLiteral("deployment-dependencies"));
+ if (!deploymentDependencies.isUndefined()) {
+ QString deploymentDependenciesString = deploymentDependencies.toString();
+ const auto dependencies = deploymentDependenciesString.splitRef(QLatin1Char(','));
+ for (const QStringRef &dependency : dependencies) {
+ QString path = options->qtInstallDirectory + QLatin1Char('/') + dependency;
+ if (QFileInfo(path).isDir()) {
+ QDirIterator iterator(path, QDirIterator::Subdirectories);
+ while (iterator.hasNext()) {
+ iterator.next();
+ if (iterator.fileInfo().isFile()) {
+ QString subPath = iterator.filePath();
+ options->qtDependencies.append(QtDependency(subPath.mid(options->qtInstallDirectory.length() + 1),
+ subPath));
+ }
+ }
+ } else {
+ options->qtDependencies.append(QtDependency(dependency.toString(), path));
+ }
+ }
+ }
+ }
+
+
+ {
+ const QJsonValue targetArchitecture = jsonObject.value(QStringLiteral("target-architecture"));
+ if (targetArchitecture.isUndefined()) {
+ fprintf(stderr, "No target architecture defined in json file.\n");
+ return false;
+ }
+ options->architecture = targetArchitecture.toString();
+ }
+
+ {
+ const QJsonValue ndk = jsonObject.value(QStringLiteral("ndk"));
+ if (ndk.isUndefined()) {
+ fprintf(stderr, "No NDK path defined in json file.\n");
+ return false;
+ }
+ options->ndkPath = ndk.toString();
+ }
+
+ {
+ const QJsonValue toolchainPrefix = jsonObject.value(QStringLiteral("toolchain-prefix"));
+ if (toolchainPrefix.isUndefined()) {
+ fprintf(stderr, "No toolchain prefix defined in json file.\n");
+ return false;
+ }
+ options->toolchainPrefix = toolchainPrefix.toString();
+ }
+
+ {
+ const QJsonValue toolPrefix = jsonObject.value(QStringLiteral("tool-prefix"));
+ if (toolPrefix.isUndefined()) {
+ fprintf(stderr, "Warning: No tool prefix defined in json file.\n");
+ options->toolPrefix = options->toolchainPrefix;
+ } else {
+ options->toolPrefix = toolPrefix.toString();
+ }
+ }
+
+ {
+ const QJsonValue toolchainVersion = jsonObject.value(QStringLiteral("toolchain-version"));
+ if (toolchainVersion.isUndefined()) {
+ fprintf(stderr, "No toolchain version defined in json file.\n");
+ return false;
+ }
+ options->toolchainVersion = toolchainVersion.toString();
+ }
+
+ {
+ const QJsonValue ndkHost = jsonObject.value(QStringLiteral("ndk-host"));
+ if (ndkHost.isUndefined()) {
+ fprintf(stderr, "No NDK host defined in json file.\n");
+ return false;
+ }
+ options->ndkHost = ndkHost.toString();
+ }
+
+ options->packageName = packageNameFromAndroidManifest(options->androidSourceDirectory + QLatin1String("/AndroidManifest.xml"));
+ if (options->packageName.isEmpty())
+ options->packageName = cleanPackageName(QString::fromLatin1("org.qtproject.example.%1").arg(QFileInfo(options->applicationBinary).baseName().mid(sizeof("lib") - 1)));
+
+ {
+ const QJsonValue extraLibs = jsonObject.value(QStringLiteral("android-extra-libs"));
+ if (!extraLibs.isUndefined())
+ options->extraLibs = extraLibs.toString().split(QLatin1Char(','), QString::SkipEmptyParts);
+ }
+
+ {
+ const QJsonValue extraPlugins = jsonObject.value(QStringLiteral("android-extra-plugins"));
+ if (!extraPlugins.isUndefined())
+ options->extraPlugins = extraPlugins.toString().split(QLatin1Char(','));
+ }
+
+ {
+ const QJsonValue stdcppPath = jsonObject.value(QStringLiteral("stdcpp-path"));
+ if (!stdcppPath.isUndefined()) {
+ options->stdCppPath = stdcppPath.toString();
+ auto name = QFileInfo(options->stdCppPath).baseName();
+ if (!name.startsWith(QLatin1String("lib"))) {
+ fprintf(stderr, "Invalid STD C++ library name.\n");
+ return false;
+ }
+ options->stdCppName = name.mid(3);
+ }
+ }
+
+ {
+ const QJsonValue qmlRootPath = jsonObject.value(QStringLiteral("qml-root-path"));
+ if (!qmlRootPath.isUndefined())
+ options->rootPath = qmlRootPath.toString();
+ }
+
+ {
+ const QJsonValue qmlImportPaths = jsonObject.value(QStringLiteral("qml-import-paths"));
+ if (!qmlImportPaths.isUndefined())
+ options->qmlImportPaths = qmlImportPaths.toString().split(QLatin1Char(','));
+ }
+ return true;
+}
+
+bool copyFiles(const QDir &sourceDirectory, const QDir &destinationDirectory, bool verbose, bool forceOverwrite = false)
+{
+ const QFileInfoList entries = sourceDirectory.entryInfoList(QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs);
+ for (const QFileInfo &entry : entries) {
+ if (entry.isDir()) {
+ QDir dir(entry.absoluteFilePath());
+ if (!destinationDirectory.mkpath(dir.dirName())) {
+ fprintf(stderr, "Cannot make directory %s in %s\n", qPrintable(dir.dirName()), qPrintable(destinationDirectory.path()));
+ return false;
+ }
+
+ if (!copyFiles(dir, QDir(destinationDirectory.path() + QLatin1String("/") + dir.dirName()), verbose, forceOverwrite))
+ return false;
+ } else {
+ QString destination = destinationDirectory.absoluteFilePath(entry.fileName());
+ if (!copyFileIfNewer(entry.absoluteFilePath(), destination, verbose, forceOverwrite))
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void cleanTopFolders(const Options &options, const QDir &srcDir, const QString &dstDir)
+{
+ const auto dirs = srcDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Dirs);
+ for (const QFileInfo &dir : dirs) {
+ if (dir.fileName() != QLatin1String("libs"))
+ deleteMissingFiles(options, dir.absoluteFilePath(), dstDir + dir.fileName());
+ }
+}
+
+void cleanAndroidFiles(const Options &options)
+{
+ if (!options.androidSourceDirectory.isEmpty())
+ cleanTopFolders(options, options.androidSourceDirectory, options.outputDirectory);
+
+ cleanTopFolders(options, options.qtInstallDirectory + QLatin1String("/src/android/templates"), options.outputDirectory);
+}
+
+bool copyAndroidTemplate(const Options &options, const QString &androidTemplate, const QString &outDirPrefix = QString())
+{
+ QDir sourceDirectory(options.qtInstallDirectory + androidTemplate);
+ if (!sourceDirectory.exists()) {
+ fprintf(stderr, "Cannot find template directory %s\n", qPrintable(sourceDirectory.absolutePath()));
+ return false;
+ }
+
+ QString outDir = options.outputDirectory + outDirPrefix;
+
+ if (!QDir::current().mkpath(outDir)) {
+ fprintf(stderr, "Cannot create output directory %s\n", qPrintable(options.outputDirectory));
+ return false;
+ }
+
+ return copyFiles(sourceDirectory, QDir(outDir), options.verbose);
+}
+
+bool copyGradleTemplate(const Options &options)
+{
+ QDir sourceDirectory(options.qtInstallDirectory + QLatin1String("/src/3rdparty/gradle"));
+ if (!sourceDirectory.exists()) {
+ fprintf(stderr, "Cannot find template directory %s\n", qPrintable(sourceDirectory.absolutePath()));
+ return false;
+ }
+
+ QString outDir(options.outputDirectory);
+ if (!QDir::current().mkpath(outDir)) {
+ fprintf(stderr, "Cannot create output directory %s\n", qPrintable(options.outputDirectory));
+ return false;
+ }
+
+ return copyFiles(sourceDirectory, QDir(outDir), options.verbose);
+}
+
+bool copyAndroidTemplate(const Options &options)
+{
+ if (options.verbose)
+ fprintf(stdout, "Copying Android package template.\n");
+
+ if (options.gradle && !copyGradleTemplate(options))
+ return false;
+
+ if (!copyAndroidTemplate(options, QLatin1String("/src/android/templates")))
+ return false;
+
+ if (options.gradle)
+ return true;
+
+ return copyAndroidTemplate(options, QLatin1String("/src/android/java"));
+}
+
+bool copyAndroidSources(const Options &options)
+{
+ if (options.androidSourceDirectory.isEmpty())
+ return true;
+
+ if (options.verbose)
+ fprintf(stdout, "Copying Android sources from project.\n");
+
+ QDir sourceDirectory(options.androidSourceDirectory);
+ if (!sourceDirectory.exists()) {
+ fprintf(stderr, "Cannot find android sources in %s", qPrintable(options.androidSourceDirectory));
+ return false;
+ }
+
+ return copyFiles(sourceDirectory, QDir(options.outputDirectory), options.verbose, true);
+}
+
+bool copyAndroidExtraLibs(const Options &options)
+{
+ if (options.extraLibs.isEmpty())
+ return true;
+
+ if (options.verbose)
+ fprintf(stdout, "Copying %d external libraries to package.\n", options.extraLibs.size());
+
+ for (const QString &extraLib : options.extraLibs) {
+ QFileInfo extraLibInfo(extraLib);
+ if (!extraLibInfo.exists()) {
+ fprintf(stderr, "External library %s does not exist!\n", qPrintable(extraLib));
+ return false;
+ }
+
+ if (!extraLibInfo.fileName().startsWith(QLatin1String("lib")) || extraLibInfo.suffix() != QLatin1String("so")) {
+ fprintf(stderr, "The file name of external library %s must begin with \"lib\" and end with the suffix \".so\".\n",
+ qPrintable(extraLib));
+ return false;
+ }
+
+ QString destinationFile(options.outputDirectory
+ + QLatin1String("/libs/")
+ + options.architecture
+ + QLatin1Char('/')
+ + extraLibInfo.fileName());
+
+ if (!copyFileIfNewer(extraLib, destinationFile, options.verbose))
+ return false;
+ }
+
+ return true;
+}
+
+QStringList allFilesInside(const QDir& current, const QDir& rootDir)
+{
+ QStringList result;
+ const auto dirs = current.entryList(QDir::Dirs|QDir::NoDotAndDotDot);
+ const auto files = current.entryList(QDir::Files);
+ result.reserve(dirs.size() + files.size());
+ for (const QString &dir : dirs) {
+ result += allFilesInside(QDir(current.filePath(dir)), rootDir);
+ }
+ for (const QString &file : files) {
+ result += rootDir.relativeFilePath(current.filePath(file));
+ }
+ return result;
+}
+
+bool copyAndroidExtraResources(const Options &options)
+{
+ if (options.extraPlugins.isEmpty())
+ return true;
+
+ if (options.verbose)
+ fprintf(stdout, "Copying %d external resources to package.\n", options.extraPlugins.size());
+
+ for (const QString &extraResource : options.extraPlugins) {
+ QFileInfo extraResourceInfo(extraResource);
+ if (!extraResourceInfo.exists() || !extraResourceInfo.isDir()) {
+ fprintf(stderr, "External resource %s does not exist or not a correct directory!\n", qPrintable(extraResource));
+ return false;
+ }
+
+ QDir resourceDir(extraResource);
+ QString assetsDir = options.outputDirectory + QStringLiteral("/assets/") + resourceDir.dirName() + QLatin1Char('/');
+ QString libsDir = options.outputDirectory + QStringLiteral("/libs/") + options.architecture + QLatin1Char('/');
+
+ const QStringList files = allFilesInside(resourceDir, resourceDir);
+ for (const QString &resourceFile : files) {
+ QString originFile(resourceDir.filePath(resourceFile));
+ QString destinationFile;
+ if (!resourceFile.endsWith(QLatin1String(".so"))) {
+ destinationFile = assetsDir + resourceFile;
+ } else {
+ destinationFile = libsDir + QStringLiteral("/lib") + QString(resourceDir.dirName() + QLatin1Char('/') + resourceFile).replace(QLatin1Char('/'), QLatin1Char('_'));
+ }
+
+ if (!copyFileIfNewer(originFile, destinationFile, options.verbose))
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool updateFile(const QString &fileName, const QHash<QString, QString> &replacements)
+{
+ QFile inputFile(fileName);
+ if (!inputFile.open(QIODevice::ReadOnly)) {
+ fprintf(stderr, "Cannot open %s for reading.\n", qPrintable(fileName));
+ return false;
+ }
+
+ // All the files we are doing substitutes in are quite small. If this
+ // ever changes, this code should be updated to be more conservative.
+ QByteArray contents = inputFile.readAll();
+
+ bool hasReplacements = false;
+ QHash<QString, QString>::const_iterator it;
+ for (it = replacements.constBegin(); it != replacements.constEnd(); ++it) {
+ if (it.key() == it.value())
+ continue; // Nothing to actually replace
+
+ forever {
+ int index = contents.indexOf(it.key().toUtf8());
+ if (index >= 0) {
+ contents.replace(index, it.key().length(), it.value().toUtf8());
+ hasReplacements = true;
+ } else {
+ break;
+ }
+ }
+ }
+
+ if (hasReplacements) {
+ inputFile.close();
+
+ if (!inputFile.open(QIODevice::WriteOnly)) {
+ fprintf(stderr, "Cannot open %s for writing.\n", qPrintable(fileName));
+ return false;
+ }
+
+ inputFile.write(contents);
+ }
+
+ return true;
+
+}
+
+bool updateLibsXml(const Options &options)
+{
+ if (options.verbose)
+ fprintf(stdout, " -- res/values/libs.xml\n");
+
+ QString fileName = options.outputDirectory + QLatin1String("/res/values/libs.xml");
+ if (!QFile::exists(fileName)) {
+ fprintf(stderr, "Cannot find %s in prepared packaged. This file is required.\n", qPrintable(fileName));
+ return false;
+ }
+
+ QString libsPath = QLatin1String("libs/") + options.architecture + QLatin1Char('/');
+
+ QString qtLibs = QLatin1String("<item>") + options.stdCppName + QLatin1String("</item>\n");
+ QString bundledInLibs;
+ QString bundledInAssets;
+ for (const Options::BundledFile &bundledFile : options.bundledFiles) {
+ if (bundledFile.second.startsWith(QLatin1String("lib/"))) {
+ QString s = bundledFile.second.mid(sizeof("lib/lib") - 1);
+ s.chop(sizeof(".so") - 1);
+ qtLibs += QString::fromLatin1("<item>%1</item>\n").arg(s);
+ } else if (bundledFile.first.startsWith(libsPath)) {
+ QString s = bundledFile.first.mid(libsPath.length());
+ bundledInLibs += QString::fromLatin1("<item>%1:%2</item>\n")
+ .arg(s).arg(bundledFile.second);
+ } else if (bundledFile.first.startsWith(QLatin1String("assets/"))) {
+ QString s = bundledFile.first.mid(sizeof("assets/") - 1);
+ bundledInAssets += QString::fromLatin1("<item>%1:%2</item>\n")
+ .arg(s).arg(bundledFile.second);
+ }
+ }
+
+ if (!options.extraPlugins.isEmpty()) {
+ for (const QString &extraRes : options.extraPlugins) {
+ QDir resourceDir(extraRes);
+ const QStringList files = allFilesInside(resourceDir, resourceDir);
+ for (const QString &file : files) {
+ QString destinationPath = resourceDir.dirName() + QLatin1Char('/') + file;
+ if (!file.endsWith(QLatin1String(".so"))) {
+ bundledInAssets += QStringLiteral("<item>%1:%1</item>\n")
+ .arg(destinationPath);
+ } else {
+ bundledInLibs += QStringLiteral("<item>lib%1:%2</item>\n")
+ .arg(QString(destinationPath).replace(QLatin1Char('/'), QLatin1Char('_')))
+ .arg(destinationPath);
+ }
+ }
+ }
+ }
+
+ QHash<QString, QString> replacements;
+ replacements[QLatin1String("<!-- %%INSERT_QT_LIBS%% -->")] = qtLibs;
+
+ if (options.deploymentMechanism == Options::Bundled) {
+ replacements[QLatin1String("<!-- %%INSERT_BUNDLED_IN_LIB%% -->")] = bundledInLibs;
+ replacements[QLatin1String("<!-- %%INSERT_BUNDLED_IN_ASSETS%% -->")] = bundledInAssets;
+ }
+
+ QString extraLibs;
+ if (!options.extraLibs.isEmpty()) {
+ for (const QString extraLib : options.extraLibs) {
+ QFileInfo extraLibInfo(extraLib);
+ QString name = extraLibInfo.fileName().mid(sizeof("lib") - 1);
+ name.chop(sizeof(".so") - 1);
+
+ extraLibs += QLatin1String("<item>") + name + QLatin1String("</item>\n");
+ }
+ }
+ replacements[QLatin1String("<!-- %%INSERT_EXTRA_LIBS%% -->")] = extraLibs;
+
+ if (!updateFile(fileName, replacements))
+ return false;
+
+ return true;
+}
+
+bool updateStringsXml(const Options &options)
+{
+ if (options.verbose)
+ fprintf(stdout, " -- res/values/strings.xml\n");
+
+ QHash<QString, QString> replacements;
+ replacements[QStringLiteral("<!-- %%INSERT_APP_NAME%% -->")] = QFileInfo(options.applicationBinary).baseName().mid(sizeof("lib") - 1);
+
+ QString fileName = options.outputDirectory + QLatin1String("/res/values/strings.xml");
+ if (!QFile::exists(fileName)) {
+ if (options.verbose)
+ fprintf(stdout, " -- Create strings.xml since it's missing.\n");
+ QFile file(fileName);
+ if (!file.open(QIODevice::WriteOnly)) {
+ fprintf(stderr, "Can't open %s for writing.\n", qPrintable(fileName));
+ return false;
+ }
+ file.write(QByteArray("<?xml version='1.0' encoding='utf-8'?><resources><string name=\"app_name\">")
+ .append(QFileInfo(options.applicationBinary).baseName().mid(sizeof("lib") - 1).toLatin1())
+ .append("</string></resources>\n"));
+ return true;
+ }
+
+ if (!updateFile(fileName, replacements))
+ return false;
+
+ if (options.gradle)
+ return true;
+
+ // ant can't (easily) build multiple res folders,
+ // so we need to replace the "<!-- %%INSERT_STRINGS -->" placeholder
+ // from the main res folder
+ QFile stringsXml(fileName);
+ if (!stringsXml.open(QIODevice::ReadOnly)) {
+ fprintf(stderr, "Cannot open %s for reading.\n", qPrintable(fileName));
+ return false;
+ }
+
+ QXmlStreamReader reader(&stringsXml);
+ while (!reader.atEnd()) {
+ reader.readNext();
+ if (reader.isStartElement() &&
+ reader.name() == QLatin1String("string") &&
+ reader.attributes().hasAttribute(QLatin1String("name")) &&
+ reader.attributes().value(QLatin1String("name")) == QLatin1String("app_name")) {
+ return true;
+ }
+ }
+
+ replacements.clear();
+ replacements[QStringLiteral("<!-- %%INSERT_STRINGS -->")] = QString::fromLatin1("<string name=\"app_name\">%1</string>\n")
+ .arg(QFileInfo(options.applicationBinary).baseName().mid(sizeof("lib") - 1));
+
+ if (!updateFile(fileName, replacements))
+ return false;
+
+ return true;
+}
+
+bool updateAndroidManifest(Options &options)
+{
+ if (options.verbose)
+ fprintf(stdout, " -- AndroidManifest.xml \n");
+
+ QStringList localLibs = options.localLibs;
+
+ // If .pro file overrides dependency detection, we need to see which platform plugin they picked
+ if (localLibs.isEmpty()) {
+ QString plugin;
+ for (const QtDependency &qtDependency : qAsConst(options.qtDependencies)) {
+ if (qtDependency.relativePath.endsWith(QLatin1String("libqtforandroid.so"))
+ || qtDependency.relativePath.endsWith(QLatin1String("libqtforandroidGL.so"))) {
+ if (!plugin.isEmpty() && plugin != qtDependency.relativePath) {
+ fprintf(stderr, "Both platform plugins libqtforandroid.so and libqtforandroidGL.so included in package. Please include only one.\n");
+ return false;
+ }
+
+ plugin = qtDependency.relativePath;
+ }
+ }
+
+ if (plugin.isEmpty()) {
+ fprintf(stderr, "No platform plugin, neither libqtforandroid.so or libqtforandroidGL.so, included in package. Please include one.\n");
+ return false;
+ }
+
+ localLibs.append(plugin);
+ if (options.verbose)
+ fprintf(stdout, " -- Using platform plugin %s\n", qPrintable(plugin));
+ }
+
+ bool usesGL = false;
+ for (const QtDependency &qtDependency : qAsConst(options.qtDependencies)) {
+ if (qtDependency.relativePath.endsWith(QLatin1String("libQt5OpenGL.so"))
+ || qtDependency.relativePath.endsWith(QLatin1String("libQt5Quick.so"))) {
+ usesGL = true;
+ break;
+ }
+ }
+
+ QHash<QString, QString> replacements;
+ replacements[QLatin1String("-- %%INSERT_APP_NAME%% --")] = QFileInfo(options.applicationBinary).baseName().mid(sizeof("lib") - 1);
+ replacements[QLatin1String("-- %%INSERT_APP_LIB_NAME%% --")] = QFileInfo(options.applicationBinary).baseName().mid(sizeof("lib") - 1);
+ replacements[QLatin1String("-- %%INSERT_LOCAL_LIBS%% --")] = localLibs.join(QLatin1Char(':'));
+ replacements[QLatin1String("-- %%INSERT_LOCAL_JARS%% --")] = options.localJars.join(QLatin1Char(':'));
+ replacements[QLatin1String("-- %%INSERT_INIT_CLASSES%% --")] = options.initClasses.join(QLatin1Char(':'));
+ replacements[QLatin1String("package=\"org.qtproject.example\"")] = QString::fromLatin1("package=\"%1\"").arg(options.packageName);
+ replacements[QLatin1String("-- %%BUNDLE_LOCAL_QT_LIBS%% --")]
+ = (options.deploymentMechanism == Options::Bundled) ? QString::fromLatin1("1") : QString::fromLatin1("0");
+ replacements[QLatin1String("-- %%USE_LOCAL_QT_LIBS%% --")]
+ = (options.deploymentMechanism != Options::Ministro) ? QString::fromLatin1("1") : QString::fromLatin1("0");
+
+ QString permissions;
+ for (const QString &permission : qAsConst(options.permissions))
+ permissions += QString::fromLatin1(" <uses-permission android:name=\"%1\" />\n").arg(permission);
+ replacements[QLatin1String("<!-- %%INSERT_PERMISSIONS -->")] = permissions;
+
+ QString features;
+ for (const QString &feature : qAsConst(options.features))
+ features += QStringLiteral(" <uses-feature android:name=\"%1\" android:required=\"false\" />\n").arg(feature);
+ if (usesGL)
+ features += QStringLiteral(" <uses-feature android:glEsVersion=\"0x00020000\" android:required=\"true\" />");
+
+ replacements[QLatin1String("<!-- %%INSERT_FEATURES -->")] = features;
+
+ QString androidManifestPath = options.outputDirectory + QLatin1String("/AndroidManifest.xml");
+ if (!updateFile(androidManifestPath, replacements))
+ return false;
+
+ // read the package, min & target sdk API levels from manifest file.
+ bool checkOldAndroidLabelString = false;
+ QFile androidManifestXml(androidManifestPath);
+ if (androidManifestXml.exists()) {
+ if (!androidManifestXml.open(QIODevice::ReadOnly)) {
+ fprintf(stderr, "Cannot open %s for reading.\n", qPrintable(androidManifestPath));
+ return false;
+ }
+
+ QXmlStreamReader reader(&androidManifestXml);
+ while (!reader.atEnd()) {
+ reader.readNext();
+
+ if (reader.isStartElement()) {
+ if (reader.name() == QLatin1String("manifest")) {
+ if (!reader.attributes().hasAttribute(QLatin1String("package"))) {
+ fprintf(stderr, "Invalid android manifest file: %s\n", qPrintable(androidManifestPath));
+ return false;
+ }
+ options.packageName = reader.attributes().value(QLatin1String("package")).toString();
+ } else if (reader.name() == QLatin1String("uses-sdk")) {
+ if (reader.attributes().hasAttribute(QLatin1String("android:minSdkVersion")))
+ if (reader.attributes().value(QLatin1String("android:minSdkVersion")).toInt() < 16) {
+ fprintf(stderr, "Invalid minSdkVersion version, minSdkVersion must be >= 16\n");
+ return false;
+ }
+ } else if ((reader.name() == QLatin1String("application") ||
+ reader.name() == QLatin1String("activity")) &&
+ reader.attributes().hasAttribute(QLatin1String("android:label")) &&
+ reader.attributes().value(QLatin1String("android:label")) == QLatin1String("@string/app_name")) {
+ checkOldAndroidLabelString = true;
+ }
+ }
+ }
+
+ if (reader.hasError()) {
+ fprintf(stderr, "Error in %s: %s\n", qPrintable(androidManifestPath), qPrintable(reader.errorString()));
+ return false;
+ }
+ } else {
+ fprintf(stderr, "No android manifest file");
+ return false;
+ }
+
+ if (checkOldAndroidLabelString)
+ updateStringsXml(options);
+
+ return true;
+}
+
+bool updateAndroidFiles(Options &options)
+{
+ if (options.verbose)
+ fprintf(stdout, "Updating Android package files with project settings.\n");
+
+ if (!updateLibsXml(options))
+ return false;
+
+ if (!updateAndroidManifest(options))
+ return false;
+
+ return true;
+}
+
+QList<QtDependency> findFilesRecursively(const Options &options, const QFileInfo &info, const QString &rootPath)
+{
+ if (!info.exists())
+ return QList<QtDependency>();
+
+ if (info.isDir()) {
+ QList<QtDependency> ret;
+
+ QDir dir(info.filePath());
+ const QStringList entries = dir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot);
+
+ for (const QString &entry : entries) {
+ QString s = info.absoluteFilePath() + QLatin1Char('/') + entry;
+ ret += findFilesRecursively(options, s, rootPath);
+ }
+
+ return ret;
+ } else {
+ return QList<QtDependency>() << QtDependency(info.absoluteFilePath().mid(rootPath.length()), info.absoluteFilePath());
+ }
+}
+
+QList<QtDependency> findFilesRecursively(const Options &options, const QString &fileName)
+{
+ QFileInfo info(options.qtInstallDirectory + QLatin1Char('/') + fileName);
+ return findFilesRecursively(options, info, options.qtInstallDirectory + QLatin1Char('/'));
+}
+
+bool readAndroidDependencyXml(Options *options,
+ const QString &moduleName,
+ QSet<QString> *usedDependencies,
+ QSet<QString> *remainingDependencies)
+{
+ QString androidDependencyName = options->qtInstallDirectory + QString::fromLatin1("/lib/%1-android-dependencies.xml").arg(moduleName);
+
+ QFile androidDependencyFile(androidDependencyName);
+ if (androidDependencyFile.exists()) {
+ if (options->verbose)
+ fprintf(stdout, "Reading Android dependencies for %s\n", qPrintable(moduleName));
+
+ if (!androidDependencyFile.open(QIODevice::ReadOnly)) {
+ fprintf(stderr, "Cannot open %s for reading.\n", qPrintable(androidDependencyName));
+ return false;
+ }
+
+ QXmlStreamReader reader(&androidDependencyFile);
+ while (!reader.atEnd()) {
+ reader.readNext();
+
+ if (reader.isStartElement()) {
+ if (reader.name() == QLatin1String("bundled")) {
+ if (!reader.attributes().hasAttribute(QLatin1String("file"))) {
+ fprintf(stderr, "Invalid android dependency file: %s\n", qPrintable(androidDependencyName));
+ return false;
+ }
+
+ QString file = reader.attributes().value(QLatin1String("file")).toString();
+
+ // Special case, since this is handled by qmlimportscanner instead
+ if (!options->rootPath.isEmpty() && (file == QLatin1String("qml") || file == QLatin1String("qml/")))
+ continue;
+
+ const QList<QtDependency> fileNames = findFilesRecursively(*options, file);
+ for (const QtDependency &fileName : fileNames) {
+ if (usedDependencies->contains(fileName.absolutePath))
+ continue;
+
+ usedDependencies->insert(fileName.absolutePath);
+
+ if (options->verbose)
+ fprintf(stdout, "Appending dependency from xml: %s\n", qPrintable(fileName.relativePath));
+
+ options->qtDependencies.append(fileName);
+ }
+ } else if (reader.name() == QLatin1String("jar")) {
+ int bundling = reader.attributes().value(QLatin1String("bundling")).toInt();
+ QString fileName = reader.attributes().value(QLatin1String("file")).toString();
+ if (bundling == (options->deploymentMechanism == Options::Bundled)) {
+ QtDependency dependency(fileName, options->qtInstallDirectory + QLatin1Char('/') + fileName);
+ if (!usedDependencies->contains(dependency.absolutePath)) {
+ options->qtDependencies.append(dependency);
+ usedDependencies->insert(dependency.absolutePath);
+ }
+ }
+
+ if (!fileName.isEmpty())
+ options->localJars.append(fileName);
+
+ if (reader.attributes().hasAttribute(QLatin1String("initClass"))) {
+ options->initClasses.append(reader.attributes().value(QLatin1String("initClass")).toString());
+ }
+ } else if (reader.name() == QLatin1String("lib")) {
+ QString fileName = reader.attributes().value(QLatin1String("file")).toString();
+ if (reader.attributes().hasAttribute(QLatin1String("replaces"))) {
+ QString replaces = reader.attributes().value(QLatin1String("replaces")).toString();
+ for (int i=0; i<options->localLibs.size(); ++i) {
+ if (options->localLibs.at(i) == replaces) {
+ options->localLibs[i] = fileName;
+ break;
+ }
+ }
+ } else if (!fileName.isEmpty()) {
+ options->localLibs.append(fileName);
+ }
+ if (fileName.endsWith(QLatin1String(".so"))) {
+ remainingDependencies->insert(fileName);
+ }
+ } else if (reader.name() == QLatin1String("permission")) {
+ QString name = reader.attributes().value(QLatin1String("name")).toString();
+ options->permissions.append(name);
+ } else if (reader.name() == QLatin1String("feature")) {
+ QString name = reader.attributes().value(QLatin1String("name")).toString();
+ options->features.append(name);
+ }
+ }
+ }
+
+ if (reader.hasError()) {
+ fprintf(stderr, "Error in %s: %s\n", qPrintable(androidDependencyName), qPrintable(reader.errorString()));
+ return false;
+ }
+ } else if (options->verbose) {
+ fprintf(stdout, "No android dependencies for %s\n", qPrintable(moduleName));
+ }
+
+ return true;
+}
+
+QStringList getQtLibsFromElf(const Options &options, const QString &fileName)
+{
+ QString readElf = options.ndkPath
+ + QLatin1String("/toolchains/")
+ + options.toolchainPrefix
+ + QLatin1Char('-')
+ + options.toolchainVersion
+ + QLatin1String("/prebuilt/")
+ + options.ndkHost
+ + QLatin1String("/bin/")
+ + options.toolPrefix
+ + QLatin1String("-readelf");
+#if defined(Q_OS_WIN32)
+ readElf += QLatin1String(".exe");
+#endif
+
+ if (!QFile::exists(readElf)) {
+ fprintf(stderr, "Command does not exist: %s\n", qPrintable(readElf));
+ return QStringList();
+ }
+
+ readElf = QString::fromLatin1("%1 -d -W %2").arg(shellQuote(readElf)).arg(shellQuote(fileName));
+
+ FILE *readElfCommand = openProcess(readElf);
+ if (readElfCommand == 0) {
+ fprintf(stderr, "Cannot execute command %s", qPrintable(readElf));
+ return QStringList();
+ }
+
+ QStringList ret;
+
+ char buffer[512];
+ while (fgets(buffer, sizeof(buffer), readElfCommand) != 0) {
+ QByteArray line = QByteArray::fromRawData(buffer, qstrlen(buffer));
+ if (line.contains("(NEEDED)") && line.contains("Shared library:") ) {
+ const int pos = line.lastIndexOf('[') + 1;
+ QString libraryName = QLatin1String("lib/") + QString::fromLatin1(line.mid(pos, line.length() - pos - 2));
+ if (QFile::exists(options.qtInstallDirectory + QLatin1Char('/') + libraryName)) {
+ ret += libraryName;
+ }
+
+ }
+ }
+
+ pclose(readElfCommand);
+
+ return ret;
+}
+
+bool readDependenciesFromElf(Options *options,
+ const QString &fileName,
+ QSet<QString> *usedDependencies,
+ QSet<QString> *remainingDependencies)
+{
+ // Get dependencies on libraries in $QTDIR/lib
+ const QStringList dependencies = getQtLibsFromElf(*options, fileName);
+
+ if (options->verbose) {
+ fprintf(stdout, "Reading dependencies from %s\n", qPrintable(fileName));
+ for (const QString &dep : dependencies)
+ fprintf(stdout, " %s\n", qPrintable(dep));
+ }
+ // Recursively add dependencies from ELF and supplementary XML information
+ QList<QString> dependenciesToCheck;
+ for (const QString &dependency : dependencies) {
+ if (usedDependencies->contains(dependency))
+ continue;
+
+ QString absoluteDependencyPath(options->qtInstallDirectory + QLatin1Char('/') + dependency);
+ usedDependencies->insert(dependency);
+ if (!readDependenciesFromElf(options,
+ absoluteDependencyPath,
+ usedDependencies,
+ remainingDependencies)) {
+ return false;
+ }
+
+ options->qtDependencies.append(QtDependency(dependency, absoluteDependencyPath));
+ if (options->verbose)
+ fprintf(stdout, "Appending dependency: %s\n", qPrintable(dependency));
+ dependenciesToCheck.append(dependency);
+ }
+
+ for (const QString &dependency : qAsConst(dependenciesToCheck)) {
+ QString qtBaseName = dependency.mid(sizeof("lib/lib") - 1);
+ qtBaseName = qtBaseName.left(qtBaseName.size() - (sizeof(".so") - 1));
+ if (!readAndroidDependencyXml(options, qtBaseName, usedDependencies, remainingDependencies)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool goodToCopy(const Options *options, const QString &file, QStringList *unmetDependencies);
+
+bool scanImports(Options *options, QSet<QString> *usedDependencies)
+{
+ if (options->verbose)
+ fprintf(stdout, "Scanning for QML imports.\n");
+
+ QString qmlImportScanner = options->qtInstallDirectory + QLatin1String("/bin/qmlimportscanner");
+#if defined(Q_OS_WIN32)
+ qmlImportScanner += QLatin1String(".exe");
+#endif
+
+ if (!QFile::exists(qmlImportScanner)) {
+ fprintf(stderr, "qmlimportscanner not found: %s\n", qPrintable(qmlImportScanner));
+ return true;
+ }
+
+ QString rootPath = options->rootPath;
+ if (rootPath.isEmpty())
+ rootPath = QFileInfo(options->inputFileName).absolutePath();
+ else
+ rootPath = QFileInfo(rootPath).absoluteFilePath();
+
+ if (!rootPath.endsWith(QLatin1Char('/')))
+ rootPath += QLatin1Char('/');
+
+ QStringList importPaths;
+ importPaths += shellQuote(options->qtInstallDirectory + QLatin1String("/qml"));
+ importPaths += rootPath;
+ for (const QString &qmlImportPath : qAsConst(options->qmlImportPaths))
+ importPaths += shellQuote(qmlImportPath);
+
+ qmlImportScanner += QString::fromLatin1(" -rootPath %1 -importPath %2")
+ .arg(shellQuote(rootPath))
+ .arg(importPaths.join(QLatin1Char(' ')));
+
+ FILE *qmlImportScannerCommand = popen(qmlImportScanner.toLocal8Bit().constData(), "r");
+ if (qmlImportScannerCommand == 0) {
+ fprintf(stderr, "Couldn't run qmlimportscanner.\n");
+ return false;
+ }
+
+ QByteArray output;
+ char buffer[512];
+ while (fgets(buffer, sizeof(buffer), qmlImportScannerCommand) != 0)
+ output += QByteArray(buffer, qstrlen(buffer));
+
+ QJsonDocument jsonDocument = QJsonDocument::fromJson(output);
+ if (jsonDocument.isNull()) {
+ fprintf(stderr, "Invalid json output from qmlimportscanner.\n");
+ return false;
+ }
+
+ QJsonArray jsonArray = jsonDocument.array();
+ for (int i=0; i<jsonArray.count(); ++i) {
+ QJsonValue value = jsonArray.at(i);
+ if (!value.isObject()) {
+ fprintf(stderr, "Invalid format of qmlimportscanner output.\n");
+ return false;
+ }
+
+ QJsonObject object = value.toObject();
+ QString path = object.value(QLatin1String("path")).toString();
+ if (path.isEmpty()) {
+ fprintf(stderr, "Warning: QML import could not be resolved in any of the import paths: %s\n",
+ qPrintable(object.value(QLatin1String("name")).toString()));
+ } else {
+ if (options->verbose)
+ fprintf(stdout, " -- Adding '%s' as QML dependency\n", path.toLocal8Bit().constData());
+
+ QFileInfo info(path);
+
+ // The qmlimportscanner sometimes outputs paths that do not exist.
+ if (!info.exists()) {
+ if (options->verbose)
+ fprintf(stdout, " -- Skipping because file does not exist.\n");
+ continue;
+ }
+
+ QString absolutePath = info.absolutePath();
+ if (!absolutePath.endsWith(QLatin1Char('/')))
+ absolutePath += QLatin1Char('/');
+
+ if (absolutePath.startsWith(rootPath)) {
+ if (options->verbose)
+ fprintf(stdout, " -- Skipping because file is in QML root path.\n");
+ continue;
+ }
+
+ QString importPathOfThisImport;
+ for (const QString &importPath : qAsConst(importPaths)) {
+#if defined(Q_OS_WIN32)
+ Qt::CaseSensitivity caseSensitivity = Qt::CaseInsensitive;
+#else
+ Qt::CaseSensitivity caseSensitivity = Qt::CaseSensitive;
+#endif
+ QString cleanImportPath = QDir::cleanPath(importPath);
+ if (info.absoluteFilePath().startsWith(cleanImportPath, caseSensitivity)) {
+ importPathOfThisImport = importPath;
+ break;
+ }
+ }
+
+ if (importPathOfThisImport.isEmpty()) {
+ fprintf(stderr, "Import found outside of import paths: %s.\n", qPrintable(info.absoluteFilePath()));
+ return false;
+ }
+
+ QDir dir(importPathOfThisImport);
+ importPathOfThisImport = dir.absolutePath() + QLatin1Char('/');
+
+ const QList<QtDependency> fileNames = findFilesRecursively(*options, info, importPathOfThisImport);
+ for (QtDependency fileName : fileNames) {
+ if (usedDependencies->contains(fileName.absolutePath))
+ continue;
+
+ usedDependencies->insert(fileName.absolutePath);
+
+ if (options->verbose)
+ fprintf(stdout, " -- Appending dependency found by qmlimportscanner: %s\n", qPrintable(fileName.absolutePath));
+
+ // Put all imports in default import path in assets
+ fileName.relativePath.prepend(QLatin1String("qml/"));
+ options->qtDependencies.append(fileName);
+
+ if (fileName.absolutePath.endsWith(QLatin1String(".so"))) {
+ QSet<QString> remainingDependencies;
+ if (!readDependenciesFromElf(options, fileName.absolutePath, usedDependencies, &remainingDependencies))
+ return false;
+
+ }
+ }
+ }
+ }
+
+ return true;
+}
+
+bool readDependencies(Options *options)
+{
+ if (options->verbose)
+ fprintf(stdout, "Detecting dependencies of application.\n");
+
+ // Override set in .pro file
+ if (!options->qtDependencies.isEmpty()) {
+ if (options->verbose)
+ fprintf(stdout, "\tDependencies explicitly overridden in .pro file. No detection needed.\n");
+ return true;
+ }
+
+ QSet<QString> usedDependencies;
+ QSet<QString> remainingDependencies;
+
+ // Add dependencies of application binary first
+ if (!readDependenciesFromElf(options, options->applicationBinary, &usedDependencies, &remainingDependencies))
+ return false;
+
+ // Jam in the dependencies of the platform plugin, since the application will crash without it
+ if (!readDependenciesFromElf(options, options->qtInstallDirectory + QLatin1String("/plugins/platforms/android/libqtforandroid.so"), &usedDependencies, &remainingDependencies))
+ return false;
+
+ QString qtDir = options->qtInstallDirectory + QLatin1Char('/');
+
+ while (!remainingDependencies.isEmpty()) {
+ QSet<QString>::iterator start = remainingDependencies.begin();
+ QString fileName = qtDir + *start;
+ remainingDependencies.erase(start);
+
+ QStringList unmetDependencies;
+ if (goodToCopy(options, fileName, &unmetDependencies)) {
+ bool ok = readDependenciesFromElf(options, fileName, &usedDependencies, &remainingDependencies);
+ if (!ok)
+ return false;
+ } else {
+ fprintf(stdout, "Skipping %s due to unmet dependencies: %s\n",
+ qPrintable(fileName),
+ qPrintable(unmetDependencies.join(QLatin1Char(','))));
+ }
+ }
+
+ QStringList::iterator it = options->localLibs.begin();
+ while (it != options->localLibs.end()) {
+ QStringList unmetDependencies;
+ if (!goodToCopy(options, qtDir + *it, &unmetDependencies)) {
+ fprintf(stdout, "Skipping %s due to unmet dependencies: %s\n",
+ qPrintable(*it),
+ qPrintable(unmetDependencies.join(QLatin1Char(','))));
+ it = options->localLibs.erase(it);
+ } else {
+ ++it;
+ }
+ }
+
+ if (!options->rootPath.isEmpty() && !scanImports(options, &usedDependencies))
+ return false;
+
+ return true;
+}
+
+bool stripFile(const Options &options, const QString &fileName)
+{
+ QString strip = options.ndkPath
+ + QLatin1String("/toolchains/")
+ + options.toolchainPrefix
+ + QLatin1Char('-')
+ + options.toolchainVersion
+ + QLatin1String("/prebuilt/")
+ + options.ndkHost
+ + QLatin1String("/bin/")
+ + options.toolPrefix
+ + QLatin1String("-strip");
+#if defined(Q_OS_WIN32)
+ strip += QLatin1String(".exe");
+#endif
+
+ if (!QFile::exists(strip)) {
+ fprintf(stderr, "Command does not exist: %s\n", qPrintable(strip));
+ return false;
+ }
+
+ strip = QString::fromLatin1("%1 %2").arg(shellQuote(strip)).arg(shellQuote(fileName));
+
+ FILE *stripCommand = openProcess(strip);
+ if (stripCommand == 0) {
+ fprintf(stderr, "Cannot execute command %s", qPrintable(strip));
+ return false;
+ }
+
+ pclose(stripCommand);
+
+ return true;
+}
+
+bool stripLibraries(const Options &options)
+{
+ if (options.verbose)
+ fprintf(stdout, "Stripping libraries to minimize size.\n");
+
+
+ QString libraryPath = options.outputDirectory
+ + QLatin1String("/libs/")
+ + options.architecture;
+ const QStringList libraries = QDir(libraryPath).entryList(QDir::Files);
+ for (const QString &library : libraries) {
+ if (library.endsWith(QLatin1String(".so"))) {
+ if (!stripFile(options, libraryPath + QLatin1Char('/') + library))
+ return false;
+ }
+ }
+
+
+ return true;
+}
+
+bool containsApplicationBinary(const Options &options)
+{
+ if (options.verbose)
+ fprintf(stdout, "Checking if application binary is in package.\n");
+
+ QFileInfo applicationBinary(options.applicationBinary);
+ QString destinationFileName = options.outputDirectory
+ + QLatin1String("/libs/")
+ + options.architecture
+ + QLatin1Char('/')
+ + applicationBinary.fileName();
+
+ if (!QFile::exists(destinationFileName)) {
+#if defined(Q_OS_WIN32)
+ QLatin1String makeTool("mingw32-make"); // Only Mingw host builds supported on Windows currently
+#else
+ QLatin1String makeTool("make");
+#endif
+
+ fprintf(stderr, "Application binary is not in output directory: %s. Please run '%s install INSTALL_ROOT=%s' first.\n",
+ qPrintable(destinationFileName),
+ qPrintable(makeTool),
+ qPrintable(options.outputDirectory));
+ return false;
+ }
+
+ return true;
+}
+
+FILE *runAdb(const Options &options, const QString &arguments)
+{
+ QString adb = options.sdkPath + QLatin1String("/platform-tools/adb");
+#if defined(Q_OS_WIN32)
+ adb += QLatin1String(".exe");
+#endif
+
+ if (!QFile::exists(adb)) {
+ fprintf(stderr, "Cannot find adb tool: %s\n", qPrintable(adb));
+ return 0;
+ }
+ QString installOption;
+ if (!options.installLocation.isEmpty())
+ installOption = QLatin1String(" -s ") + shellQuote(options.installLocation);
+
+ adb = QString::fromLatin1("%1%2 %3").arg(shellQuote(adb)).arg(installOption).arg(arguments);
+
+ if (options.verbose)
+ fprintf(stdout, "Running command \"%s\"\n", adb.toLocal8Bit().constData());
+
+ FILE *adbCommand = openProcess(adb);
+ if (adbCommand == 0) {
+ fprintf(stderr, "Cannot start adb: %s\n", qPrintable(adb));
+ return 0;
+ }
+
+ return adbCommand;
+}
+
+bool goodToCopy(const Options *options, const QString &file, QStringList *unmetDependencies)
+{
+ if (!file.endsWith(QLatin1String(".so")))
+ return true;
+
+ bool ret = true;
+ const auto libs = getQtLibsFromElf(*options, file);
+ for (const QString &lib : libs) {
+ if (!options->qtDependencies.contains(QtDependency(lib, options->qtInstallDirectory + QLatin1Char('/') + lib))) {
+ ret = false;
+ unmetDependencies->append(lib);
+ }
+ }
+
+ return ret;
+}
+
+bool copyQtFiles(Options *options)
+{
+ if (options->verbose) {
+ switch (options->deploymentMechanism) {
+ case Options::Bundled:
+ fprintf(stdout, "Copying %d dependencies from Qt into package.\n", options->qtDependencies.size());
+ break;
+ case Options::Ministro:
+ fprintf(stdout, "Setting %d dependencies from Qt in package.\n", options->qtDependencies.size());
+ break;
+ };
+ }
+
+ if (!options->build)
+ return true;
+
+ QString libsDirectory = QLatin1String("libs/");
+
+ // Copy other Qt dependencies
+ QString libDestinationDirectory = libsDirectory + options->architecture + QLatin1Char('/');
+ QString assetsDestinationDirectory = QLatin1String("assets/--Added-by-androiddeployqt--/");
+ for (const QtDependency &qtDependency : qAsConst(options->qtDependencies)) {
+ QString sourceFileName = qtDependency.absolutePath;
+ QString destinationFileName;
+
+ if (qtDependency.relativePath.endsWith(QLatin1String(".so"))) {
+ QString garbledFileName;
+ if (qtDependency.relativePath.startsWith(QLatin1String("lib/"))) {
+ garbledFileName = qtDependency.relativePath.mid(sizeof("lib/") - 1);
+ } else {
+ garbledFileName = QLatin1String("lib")
+ + QString(qtDependency.relativePath).replace(QLatin1Char('/'), QLatin1Char('_'));
+
+ }
+ destinationFileName = libDestinationDirectory + garbledFileName;
+
+ } else if (qtDependency.relativePath.startsWith(QLatin1String("jar/"))) {
+ destinationFileName = libsDirectory + qtDependency.relativePath.mid(sizeof("jar/") - 1);
+ } else {
+ destinationFileName = assetsDestinationDirectory + qtDependency.relativePath;
+ }
+
+ if (!QFile::exists(sourceFileName)) {
+ fprintf(stderr, "Source Qt file does not exist: %s.\n", qPrintable(sourceFileName));
+ return false;
+ }
+
+ QStringList unmetDependencies;
+ if (!goodToCopy(options, sourceFileName, &unmetDependencies)) {
+ fprintf(stdout, " -- Skipping %s. It has unmet dependencies: %s.\n",
+ qPrintable(sourceFileName),
+ qPrintable(unmetDependencies.join(QLatin1Char(','))));
+ continue;
+ }
+
+ if (options->deploymentMechanism == Options::Bundled
+ && !copyFileIfNewer(sourceFileName,
+ options->outputDirectory + QLatin1Char('/') + destinationFileName,
+ options->verbose)) {
+ return false;
+ }
+
+ options->bundledFiles += qMakePair(destinationFileName, qtDependency.relativePath);
+ }
+
+ return true;
+}
+
+QStringList getLibraryProjectsInOutputFolder(const Options &options)
+{
+ QStringList ret;
+
+ QFile file(options.outputDirectory + QLatin1String("/project.properties"));
+ if (file.open(QIODevice::ReadOnly)) {
+ while (!file.atEnd()) {
+ QByteArray line = file.readLine().trimmed();
+ if (line.startsWith("android.library.reference")) {
+ int equalSignIndex = line.indexOf('=');
+ if (equalSignIndex >= 0) {
+ QString path = QString::fromLocal8Bit(line.mid(equalSignIndex + 1));
+
+ QFileInfo info(options.outputDirectory + QLatin1Char('/') + path);
+ if (QDir::isRelativePath(path)
+ && info.exists()
+ && info.isDir()
+ && info.canonicalFilePath().startsWith(options.outputDirectory)) {
+ ret += info.canonicalFilePath();
+ }
+ }
+ }
+ }
+ }
+
+ return ret;
+}
+
+bool createAndroidProject(const Options &options)
+{
+ if (options.verbose)
+ fprintf(stdout, "Running Android tool to create package definition.\n");
+
+ QString androidToolExecutable = options.sdkPath + QLatin1String("/tools/android");
+#if defined(Q_OS_WIN32)
+ androidToolExecutable += QLatin1String(".bat");
+#endif
+
+ if (!QFile::exists(androidToolExecutable)) {
+ fprintf(stderr, "Cannot find Android tool: %s\n", qPrintable(androidToolExecutable));
+ return false;
+ }
+
+ QString androidTool = QString::fromLatin1("%1 update project --path %2 --target %3 --name QtApp")
+ .arg(shellQuote(androidToolExecutable))
+ .arg(shellQuote(options.outputDirectory))
+ .arg(shellQuote(options.androidPlatform));
+
+ if (options.verbose)
+ fprintf(stdout, " -- Command: %s\n", qPrintable(androidTool));
+
+ FILE *androidToolCommand = openProcess(androidTool);
+ if (androidToolCommand == 0) {
+ fprintf(stderr, "Cannot run command '%s'\n", qPrintable(androidTool));
+ return false;
+ }
+
+ pclose(androidToolCommand);
+
+ // If the project has subprojects inside the current folder, we need to also run android update on these.
+ const QStringList libraryProjects = getLibraryProjectsInOutputFolder(options);
+ for (const QString &libraryProject : libraryProjects) {
+ if (options.verbose)
+ fprintf(stdout, "Updating subproject %s\n", qPrintable(libraryProject));
+
+ androidTool = QString::fromLatin1("%1 update lib-project --path %2 --target %3")
+ .arg(shellQuote(androidToolExecutable))
+ .arg(shellQuote(libraryProject))
+ .arg(shellQuote(options.androidPlatform));
+
+ if (options.verbose)
+ fprintf(stdout, " -- Command: %s\n", qPrintable(androidTool));
+
+ FILE *androidToolCommand = popen(androidTool.toLocal8Bit().constData(), "r");
+ if (androidToolCommand == 0) {
+ fprintf(stderr, "Cannot run command '%s'\n", qPrintable(androidTool));
+ return false;
+ }
+
+ pclose(androidToolCommand);
+ }
+
+ return true;
+}
+
+QString findInPath(const QString &fileName)
+{
+ const QString path = QString::fromLocal8Bit(qgetenv("PATH"));
+#if defined(Q_OS_WIN32)
+ QLatin1Char separator(';');
+#else
+ QLatin1Char separator(':');
+#endif
+
+ const QStringList paths = path.split(separator);
+ for (const QString &path : paths) {
+ QFileInfo fileInfo(path + QLatin1Char('/') + fileName);
+ if (fileInfo.exists() && fileInfo.isFile() && fileInfo.isExecutable())
+ return path + QLatin1Char('/') + fileName;
+ }
+
+ return QString();
+}
+
+bool buildAntProject(const Options &options)
+{
+ if (options.verbose)
+ fprintf(stdout, "Building Android package using ant.\n");
+
+ QString antTool = options.antTool;
+ if (antTool.isEmpty()) {
+#if defined(Q_OS_WIN32)
+ antTool = findInPath(QLatin1String("ant.bat"));
+#else
+ antTool = findInPath(QLatin1String("ant"));
+#endif
+ }
+
+ if (antTool.isEmpty()) {
+ fprintf(stderr, "Cannot find ant in PATH. Please use --ant option to pass in the correct path.\n");
+ return false;
+ }
+
+ if (options.verbose)
+ fprintf(stdout, "Using ant: %s\n", qPrintable(antTool));
+
+ QString oldPath = QDir::currentPath();
+ if (!QDir::setCurrent(options.outputDirectory)) {
+ fprintf(stderr, "Cannot current path to %s\n", qPrintable(options.outputDirectory));
+ return false;
+ }
+
+ QString ant = QString::fromLatin1("%1 %2").arg(shellQuote(antTool)).arg(options.releasePackage ? QLatin1String(" release") : QLatin1String(" debug"));
+
+ FILE *antCommand = openProcess(ant);
+ if (antCommand == 0) {
+ fprintf(stderr, "Cannot run ant command: %s\n.", qPrintable(ant));
+ return false;
+ }
+
+ char buffer[512];
+ while (fgets(buffer, sizeof(buffer), antCommand) != 0) {
+ fprintf(stdout, "%s", buffer);
+ fflush(stdout);
+ }
+
+ int errorCode = pclose(antCommand);
+ if (errorCode != 0) {
+ fprintf(stderr, "Building the android package failed!\n");
+ if (!options.verbose)
+ fprintf(stderr, " -- For more information, run this command with --verbose.\n");
+ return false;
+ }
+
+ if (!QDir::setCurrent(oldPath)) {
+ fprintf(stderr, "Cannot change back to old path: %s\n", qPrintable(oldPath));
+ return false;
+ }
+
+ return true;
+}
+
+typedef QMap<QByteArray, QByteArray> GradleProperties;
+
+static GradleProperties readGradleProperties(const QString &path)
+{
+ GradleProperties properties;
+ QFile file(path);
+ if (!file.open(QIODevice::ReadOnly))
+ return properties;
+
+ const auto lines = file.readAll().split('\n');
+ for (const QByteArray &line : lines) {
+ if (line.trimmed().startsWith('#'))
+ continue;
+
+ QList<QByteArray> prop(line.split('='));
+ if (prop.size() > 1)
+ properties[prop.at(0).trimmed()] = prop.at(1).trimmed();
+ }
+ file.close();
+ return properties;
+}
+
+static bool mergeGradleProperties(const QString &path, GradleProperties properties)
+{
+ QFile::remove(path + QLatin1Char('~'));
+ QFile::rename(path, path + QLatin1Char('~'));
+ QFile file(path);
+ if (!file.open(QIODevice::Truncate | QIODevice::WriteOnly | QIODevice::Text)) {
+ fprintf(stderr, "Can't open file: %s for writing\n", qPrintable(file.fileName()));
+ return false;
+ }
+
+ QFile oldFile(path + QLatin1Char('~'));
+ if (oldFile.open(QIODevice::ReadOnly)) {
+ while (!oldFile.atEnd()) {
+ QByteArray line(oldFile.readLine());
+ QList<QByteArray> prop(line.split('='));
+ if (prop.size() > 1) {
+ GradleProperties::iterator it = properties.find(prop.at(0).trimmed());
+ if (it != properties.end()) {
+ file.write(it.key() + '=' + it.value() + '\n');
+ properties.erase(it);
+ continue;
+ }
+ }
+ file.write(line);
+ }
+ oldFile.close();
+ }
+
+ for (GradleProperties::const_iterator it = properties.begin(); it != properties.end(); ++it)
+ file.write(it.key() + '=' + it.value() + '\n');
+
+ file.close();
+ return true;
+}
+
+bool buildGradleProject(const Options &options)
+{
+ GradleProperties localProperties;
+ localProperties["sdk.dir"] = options.sdkPath.toLocal8Bit();
+
+ if (!mergeGradleProperties(options.outputDirectory + QLatin1String("local.properties"), localProperties))
+ return false;
+
+ QString gradlePropertiesPath = options.outputDirectory + QLatin1String("gradle.properties");
+ GradleProperties gradleProperties = readGradleProperties(gradlePropertiesPath);
+ gradleProperties["buildDir"] = "build";
+ gradleProperties["qt5AndroidDir"] = (options.qtInstallDirectory + QLatin1String("/src/android/java")).toUtf8();
+ gradleProperties["androidCompileSdkVersion"] = options.androidPlatform.split(QLatin1Char('-')).last().toLocal8Bit();
+ if (gradleProperties["androidBuildToolsVersion"].isEmpty())
+ gradleProperties["androidBuildToolsVersion"] = options.sdkBuildToolsVersion.toLocal8Bit();
+
+ if (!mergeGradleProperties(gradlePropertiesPath, gradleProperties))
+ return false;
+
+#if defined(Q_OS_WIN32)
+ QString gradlePath(options.outputDirectory + QLatin1String("gradlew.bat"));
+#else
+ QString gradlePath(options.outputDirectory + QLatin1String("gradlew"));
+ {
+ QFile f(gradlePath);
+ if (!f.setPermissions(f.permissions() | QFileDevice::ExeUser))
+ fprintf(stderr, "Cannot set permissions %s\n", qPrintable(gradlePath));
+ }
+#endif
+
+ QString oldPath = QDir::currentPath();
+ if (!QDir::setCurrent(options.outputDirectory)) {
+ fprintf(stderr, "Cannot current path to %s\n", qPrintable(options.outputDirectory));
+ return false;
+ }
+
+ QString commandLine = QString::fromLatin1("%1 --no-daemon %2").arg(shellQuote(gradlePath)).arg(options.releasePackage ? QLatin1String(" assembleRelease") : QLatin1String(" assembleDebug"));
+ if (options.verbose)
+ commandLine += QLatin1String(" --info");
+
+ FILE *gradleCommand = openProcess(commandLine);
+ if (gradleCommand == 0) {
+ fprintf(stderr, "Cannot run gradle command: %s\n.", qPrintable(commandLine));
+ return false;
+ }
+
+ char buffer[512];
+ while (fgets(buffer, sizeof(buffer), gradleCommand) != 0) {
+ fprintf(stdout, "%s", buffer);
+ fflush(stdout);
+ }
+
+ int errorCode = pclose(gradleCommand);
+ if (errorCode != 0) {
+ fprintf(stderr, "Building the android package failed!\n");
+ if (!options.verbose)
+ fprintf(stderr, " -- For more information, run this command with --verbose.\n");
+ return false;
+ }
+
+ if (!QDir::setCurrent(oldPath)) {
+ fprintf(stderr, "Cannot change back to old path: %s\n", qPrintable(oldPath));
+ return false;
+ }
+
+ return true;
+}
+
+bool buildAndroidProject(const Options &options)
+{
+ return options.gradle ? buildGradleProject(options)
+ : buildAntProject(options);
+}
+
+bool uninstallApk(const Options &options)
+{
+ if (options.verbose)
+ fprintf(stdout, "Uninstalling old Android package %s if present.\n", qPrintable(options.packageName));
+
+
+ FILE *adbCommand = runAdb(options, QLatin1String(" uninstall ") + shellQuote(options.packageName));
+ if (adbCommand == 0)
+ return false;
+
+ if (options.verbose || mustReadOutputAnyway) {
+ char buffer[512];
+ while (fgets(buffer, sizeof(buffer), adbCommand) != 0)
+ if (options.verbose)
+ fprintf(stdout, "%s", buffer);
+ }
+
+ int returnCode = pclose(adbCommand);
+ if (returnCode != 0) {
+ fprintf(stderr, "Warning: Uninstall failed!\n");
+ if (!options.verbose)
+ fprintf(stderr, " -- Run with --verbose for more information.\n");
+ return false;
+ }
+
+ return true;
+}
+
+enum PackageType {
+ UnsignedAPK,
+ SignedAPK
+};
+
+QString apkPath(const Options &options, PackageType pt)
+{
+ QString path(options.outputDirectory);
+ if (options.gradle)
+ path += QLatin1String("/build/outputs/apk/") + QDir(options.outputDirectory).dirName() + QLatin1Char('-');
+ else
+ path += QLatin1String("/bin/QtApp-");
+ if (options.releasePackage) {
+ path += QLatin1String("release-");
+ if (pt == UnsignedAPK)
+ path += QLatin1String("un");
+ path += QLatin1String("signed.apk");
+ } else {
+ path += QLatin1String("debug");
+ if (pt == SignedAPK)
+ path += QLatin1String("-signed");
+ path += QLatin1String(".apk");
+ }
+ return shellQuote(path);
+}
+
+bool installApk(const Options &options)
+{
+ fflush(stdout);
+ // Uninstall if necessary
+ if (options.uninstallApk)
+ uninstallApk(options);
+
+ if (options.verbose)
+ fprintf(stdout, "Installing Android package to device.\n");
+
+ FILE *adbCommand = runAdb(options,
+ QLatin1String(" install -r ")
+ + apkPath(options, options.keyStore.isEmpty() ? UnsignedAPK
+ : SignedAPK));
+ if (adbCommand == 0)
+ return false;
+
+ if (options.verbose || mustReadOutputAnyway) {
+ char buffer[512];
+ while (fgets(buffer, sizeof(buffer), adbCommand) != 0)
+ if (options.verbose)
+ fprintf(stdout, "%s", buffer);
+ }
+
+ int returnCode = pclose(adbCommand);
+ if (returnCode != 0) {
+ fprintf(stderr, "Installing to device failed!\n");
+ if (!options.verbose)
+ fprintf(stderr, " -- Run with --verbose for more information.\n");
+ return false;
+ }
+
+ return true;
+}
+
+bool copyStdCpp(Options *options)
+{
+ if (options->verbose)
+ fprintf(stdout, "Copying STL library\n");
+
+ QString filePath = !options->stdCppPath.isEmpty() ? options->stdCppPath
+ : options->ndkPath
+ + QLatin1String("/sources/cxx-stl/gnu-libstdc++/")
+ + options->toolchainVersion
+ + QLatin1String("/libs/")
+ + options->architecture
+ + QLatin1String("/libgnustl_shared.so");
+ if (!QFile::exists(filePath)) {
+ fprintf(stderr, "STL library does not exist at %s\n", qPrintable(filePath));
+ return false;
+ }
+
+ const QString destinationDirectory = options->outputDirectory
+ + QLatin1String("/libs/") + options->architecture;
+
+ if (!copyFileIfNewer(filePath, destinationDirectory + QLatin1String("/lib")
+ + options->stdCppName + QLatin1String(".so"),
+ options->verbose)) {
+ return false;
+ }
+
+ return true;
+}
+
+bool jarSignerSignPackage(const Options &options)
+{
+ if (options.verbose)
+ fprintf(stdout, "Signing Android package.\n");
+
+ QString jdkPath = options.jdkPath;
+
+ if (jdkPath.isEmpty())
+ jdkPath = QString::fromLocal8Bit(qgetenv("JAVA_HOME"));
+
+#if defined(Q_OS_WIN32)
+ QString jarSignerTool = QString::fromLatin1("jarsigner.exe");
+#else
+ QString jarSignerTool = QString::fromLatin1("jarsigner");
+#endif
+
+ if (jdkPath.isEmpty() || !QFile::exists(jdkPath + QLatin1String("/bin/") + jarSignerTool))
+ jarSignerTool = findInPath(jarSignerTool);
+ else
+ jarSignerTool = jdkPath + QLatin1String("/bin/") + jarSignerTool;
+
+ if (!QFile::exists(jarSignerTool)) {
+ fprintf(stderr, "Cannot find jarsigner in JAVA_HOME or PATH. Please use --jdk option to pass in the correct path to JDK.\n");
+ return false;
+ }
+
+ jarSignerTool = QString::fromLatin1("%1 -sigalg %2 -digestalg %3 -keystore %4")
+ .arg(shellQuote(jarSignerTool)).arg(shellQuote(options.sigAlg)).arg(shellQuote(options.digestAlg)).arg(shellQuote(options.keyStore));
+
+ if (!options.keyStorePassword.isEmpty())
+ jarSignerTool += QString::fromLatin1(" -storepass %1").arg(shellQuote(options.keyStorePassword));
+
+ if (!options.storeType.isEmpty())
+ jarSignerTool += QString::fromLatin1(" -storetype %1").arg(shellQuote(options.storeType));
+
+ if (!options.keyPass.isEmpty())
+ jarSignerTool += QString::fromLatin1(" -keypass %1").arg(shellQuote(options.keyPass));
+
+ if (!options.sigFile.isEmpty())
+ jarSignerTool += QString::fromLatin1(" -sigfile %1").arg(shellQuote(options.sigFile));
+
+ if (!options.signedJar.isEmpty())
+ jarSignerTool += QString::fromLatin1(" -signedjar %1").arg(shellQuote(options.signedJar));
+
+ if (!options.tsaUrl.isEmpty())
+ jarSignerTool += QString::fromLatin1(" -tsa %1").arg(shellQuote(options.tsaUrl));
+
+ if (!options.tsaCert.isEmpty())
+ jarSignerTool += QString::fromLatin1(" -tsacert %1").arg(shellQuote(options.tsaCert));
+
+ if (options.internalSf)
+ jarSignerTool += QLatin1String(" -internalsf");
+
+ if (options.sectionsOnly)
+ jarSignerTool += QLatin1String(" -sectionsonly");
+
+ if (options.protectedAuthenticationPath)
+ jarSignerTool += QLatin1String(" -protected");
+
+ jarSignerTool += QString::fromLatin1(" %1 %2")
+ .arg(apkPath(options, UnsignedAPK))
+ .arg(shellQuote(options.keyStoreAlias));
+
+ FILE *jarSignerCommand = openProcess(jarSignerTool);
+ if (jarSignerCommand == 0) {
+ fprintf(stderr, "Couldn't run jarsigner.\n");
+ return false;
+ }
+
+ if (options.verbose) {
+ char buffer[512];
+ while (fgets(buffer, sizeof(buffer), jarSignerCommand) != 0)
+ fprintf(stdout, "%s", buffer);
+ }
+
+ int errorCode = pclose(jarSignerCommand);
+ if (errorCode != 0) {
+ fprintf(stderr, "jarsigner command failed.\n");
+ if (!options.verbose)
+ fprintf(stderr, " -- Run with --verbose for more information.\n");
+ return false;
+ }
+
+ QString zipAlignTool = options.sdkPath + QLatin1String("/tools/zipalign");
+#if defined(Q_OS_WIN32)
+ zipAlignTool += QLatin1String(".exe");
+#endif
+
+ if (!QFile::exists(zipAlignTool)) {
+ zipAlignTool = options.sdkPath + QLatin1String("/build-tools/") + options.sdkBuildToolsVersion + QLatin1String("/zipalign");
+#if defined(Q_OS_WIN32)
+ zipAlignTool += QLatin1String(".exe");
+#endif
+ if (!QFile::exists(zipAlignTool)) {
+ fprintf(stderr, "zipalign tool not found: %s\n", qPrintable(zipAlignTool));
+ return false;
+ }
+ }
+
+ zipAlignTool = QString::fromLatin1("%1%2 -f 4 %3 %4")
+ .arg(shellQuote(zipAlignTool))
+ .arg(options.verbose ? QString::fromLatin1(" -v") : QString())
+ .arg(apkPath(options, UnsignedAPK))
+ .arg(apkPath(options, SignedAPK));
+
+ FILE *zipAlignCommand = openProcess(zipAlignTool);
+ if (zipAlignCommand == 0) {
+ fprintf(stderr, "Couldn't run zipalign.\n");
+ return false;
+ }
+
+ char buffer[512];
+ while (fgets(buffer, sizeof(buffer), zipAlignCommand) != 0)
+ fprintf(stdout, "%s", buffer);
+
+ errorCode = pclose(zipAlignCommand);
+ if (errorCode != 0) {
+ fprintf(stderr, "zipalign command failed.\n");
+ if (!options.verbose)
+ fprintf(stderr, " -- Run with --verbose for more information.\n");
+ return false;
+ }
+
+ return QFile::remove(apkPath(options, UnsignedAPK));
+}
+
+bool signPackage(const Options &options)
+{
+ QString apksignerTool = options.sdkPath + QLatin1String("/build-tools/") + options.sdkBuildToolsVersion + QLatin1String("/apksigner");
+#if defined(Q_OS_WIN32)
+ apksignerTool += QLatin1String(".bat");
+#endif
+
+ if (options.jarSigner || !QFile::exists(apksignerTool))
+ return jarSignerSignPackage(options);
+
+ // APKs signed with apksigner must not be changed after they're signed, therefore we need to zipalign it before we sign it.
+
+ QString zipAlignTool = options.sdkPath + QLatin1String("/tools/zipalign");
+#if defined(Q_OS_WIN32)
+ zipAlignTool += QLatin1String(".exe");
+#endif
+
+ if (!QFile::exists(zipAlignTool)) {
+ zipAlignTool = options.sdkPath + QLatin1String("/build-tools/") + options.sdkBuildToolsVersion + QLatin1String("/zipalign");
+#if defined(Q_OS_WIN32)
+ zipAlignTool += QLatin1String(".exe");
+#endif
+ if (!QFile::exists(zipAlignTool)) {
+ fprintf(stderr, "zipalign tool not found: %s\n", qPrintable(zipAlignTool));
+ return false;
+ }
+ }
+
+ zipAlignTool = QString::fromLatin1("%1%2 -f 4 %3 %4")
+ .arg(shellQuote(zipAlignTool))
+ .arg(options.verbose ? QString::fromLatin1(" -v") : QString())
+ .arg(apkPath(options, UnsignedAPK))
+ .arg(apkPath(options, SignedAPK));
+
+ FILE *zipAlignCommand = openProcess(zipAlignTool);
+ if (zipAlignCommand == 0) {
+ fprintf(stderr, "Couldn't run zipalign.\n");
+ return false;
+ }
+
+ char buffer[512];
+ while (fgets(buffer, sizeof(buffer), zipAlignCommand) != 0)
+ fprintf(stdout, "%s", buffer);
+
+ int errorCode = pclose(zipAlignCommand);
+ if (errorCode != 0) {
+ fprintf(stderr, "zipalign command failed.\n");
+ if (!options.verbose)
+ fprintf(stderr, " -- Run with --verbose for more information.\n");
+ return false;
+ }
+
+ QString apkSignerCommandLine = QString::fromLatin1("%1 sign --ks %2")
+ .arg(shellQuote(apksignerTool)).arg(shellQuote(options.keyStore));
+
+ if (!options.keyStorePassword.isEmpty())
+ apkSignerCommandLine += QString::fromLatin1(" --ks-pass pass:%1").arg(shellQuote(options.keyStorePassword));
+
+ if (!options.keyStoreAlias.isEmpty())
+ apkSignerCommandLine += QString::fromLatin1(" --ks-key-alias %1").arg(shellQuote(options.keyStoreAlias));
+
+ if (!options.keyPass.isEmpty())
+ apkSignerCommandLine += QString::fromLatin1(" --key-pass pass:%1").arg(shellQuote(options.keyPass));
+
+ if (options.verbose)
+ apkSignerCommandLine += QLatin1String(" --verbose");
+
+ apkSignerCommandLine += QString::fromLatin1(" %1")
+ .arg(apkPath(options, SignedAPK));
+
+ auto apkSignerRunner = [&] {
+ FILE *apkSignerCommand = openProcess(apkSignerCommandLine);
+ if (apkSignerCommand == 0) {
+ fprintf(stderr, "Couldn't run apksigner.\n");
+ return false;
+ }
+
+ char buffer[512];
+ while (fgets(buffer, sizeof(buffer), apkSignerCommand) != 0)
+ fprintf(stdout, "%s", buffer);
+
+ errorCode = pclose(apkSignerCommand);
+ if (errorCode != 0) {
+ fprintf(stderr, "apksigner command failed.\n");
+ if (!options.verbose)
+ fprintf(stderr, " -- Run with --verbose for more information.\n");
+ return false;
+ }
+ return true;
+ };
+
+ // Sign the package
+ if (!apkSignerRunner())
+ return false;
+
+ apkSignerCommandLine = QString::fromLatin1("%1 verify --verbose %2")
+ .arg(shellQuote(apksignerTool)).arg(apkPath(options, SignedAPK));
+
+ // Verify the package and remove the unsigned apk
+ return apkSignerRunner() && QFile::remove(apkPath(options, UnsignedAPK));
+}
+
+bool copyGdbServer(const Options &options)
+{
+ if (options.verbose)
+ fprintf(stdout, "Copying gdbserver into package.\n");
+
+ QString architectureSubDirectory;
+ if (options.architecture == QLatin1String("arm64-v8a"))
+ architectureSubDirectory = QLatin1String("android-arm64");
+ else if (options.architecture.startsWith(QLatin1String("arm")))
+ architectureSubDirectory = QLatin1String("android-arm");
+ else
+ architectureSubDirectory = QLatin1String("android-") + options.architecture;
+
+ QString gdbServerBinary = options.ndkPath
+ + QLatin1String("/prebuilt/")
+ + architectureSubDirectory
+ + QLatin1String("/gdbserver/gdbserver");
+ if (!QFile::exists(gdbServerBinary)) {
+ fprintf(stderr, "Cannot find gdbserver at %s.\n", qPrintable(gdbServerBinary));
+ return false;
+ }
+
+ QString gdbServerTarget = options.outputDirectory + QLatin1String("/libs/") + options.architecture;
+
+ if (!copyFileIfNewer(gdbServerBinary,
+ gdbServerTarget + QLatin1String("/gdbserver"),
+ options.verbose)
+ || !copyFileIfNewer(gdbServerBinary,
+ gdbServerTarget + QLatin1String("/libgdbserver.so"),
+ options.verbose)) {
+ return false;
+ }
+
+ QString addedByAndroidDeployQtPath = options.outputDirectory + QLatin1String("/assets/--Added-by-androiddeployqt--/");
+ if (!QDir().mkpath(addedByAndroidDeployQtPath)) {
+ fprintf(stderr, "Failed to create directory '%s'", qPrintable(addedByAndroidDeployQtPath));
+ return false;
+ }
+ QFile f(addedByAndroidDeployQtPath + QLatin1String("debugger.command"));
+ if (!f.open(QIODevice::WriteOnly)) {
+ fprintf(stderr, "Failed to create directory '%s'", qPrintable(addedByAndroidDeployQtPath));
+ return false;
+ }
+ f.write("lib/libgdbserver.so --multi +");
+ f.close();
+
+ return true;
+}
+
+bool generateAssetsFileList(const Options &options)
+{
+ if (options.verbose)
+ fprintf(stdout, "Pregenerating entry list for assets file engine.\n");
+
+ QString assetsPath = options.outputDirectory + QLatin1String("/assets/");
+ QString addedByAndroidDeployQtPath = assetsPath + QLatin1String("--Added-by-androiddeployqt--/");
+ if (!QDir().mkpath(addedByAndroidDeployQtPath)) {
+ fprintf(stderr, "Failed to create directory '%s'", qPrintable(addedByAndroidDeployQtPath));
+ return false;
+ }
+
+ QFile file(addedByAndroidDeployQtPath + QLatin1String("/qt_cache_pregenerated_file_list"));
+ if (file.open(QIODevice::WriteOnly)) {
+ QDirIterator dirIterator(assetsPath,
+ QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot,
+ QDirIterator::Subdirectories);
+
+ QHash<QString, QStringList> directoryContents;
+ while (dirIterator.hasNext()) {
+ const QString name = dirIterator.next().mid(assetsPath.length());
+
+ int slashIndex = name.lastIndexOf(QLatin1Char('/'));
+ QString pathName = slashIndex >= 0 ? name.left(slashIndex) : QString::fromLatin1("/");
+ QString fileName = slashIndex >= 0 ? name.mid(pathName.length() + 1) : name;
+
+ if (!fileName.isEmpty() && dirIterator.fileInfo().isDir() && !fileName.endsWith(QLatin1Char('/')))
+ fileName += QLatin1Char('/');
+
+ if (fileName.isEmpty() && !directoryContents.contains(pathName))
+ directoryContents[pathName] = QStringList();
+ else if (!fileName.isEmpty())
+ directoryContents[pathName].append(fileName);
+ }
+
+ QDataStream stream(&file);
+ stream.setVersion(QDataStream::Qt_5_3);
+ for (auto it = directoryContents.cbegin(), end = directoryContents.cend(); it != end; ++it) {
+ const QStringList &entryList = it.value();
+ stream << it.key() << entryList.size();
+ for (const QString &entry : entryList)
+ stream << entry;
+ }
+ } else {
+ fprintf(stderr, "Pregenerating entry list for assets file engine failed!\n");
+ return false;
+ }
+
+ return true;
+}
+
+enum ErrorCode
+{
+ Success,
+ SyntaxErrorOrHelpRequested = 1,
+ CannotReadInputFile = 2,
+ CannotCopyAndroidTemplate = 3,
+ CannotReadDependencies = 4,
+ CannotCopyGnuStl = 5,
+ CannotCopyQtFiles = 6,
+ CannotFindApplicationBinary = 7,
+ CannotCopyGdbServer = 8,
+ CannotStripLibraries = 9,
+ CannotCopyAndroidExtraLibs = 10,
+ CannotCopyAndroidSources = 11,
+ CannotUpdateAndroidFiles = 12,
+ CannotCreateAndroidProject = 13,
+ CannotBuildAndroidProject = 14,
+ CannotSignPackage = 15,
+ CannotInstallApk = 16,
+ CannotGenerateAssetsFileList = 18,
+ CannotCopyAndroidExtraResources = 19
+};
+
+int main(int argc, char *argv[])
+{
+ QCoreApplication a(argc, argv);
+
+ Options options = parseOptions();
+ if (options.helpRequested || options.outputDirectory.isEmpty()) {
+ printHelp();
+ return SyntaxErrorOrHelpRequested;
+ }
+
+ options.timer.start();
+
+ if (!readInputFile(&options))
+ return CannotReadInputFile;
+
+ if (Q_UNLIKELY(options.timing))
+ fprintf(stdout, "[TIMING] %d ms: Read input file\n", options.timer.elapsed());
+
+ fprintf(stdout,
+// "012345678901234567890123456789012345678901234567890123456789012345678901"
+ "Generating Android Package\n"
+ " Input file: %s\n"
+ " Output directory: %s\n"
+ " Application binary: %s\n"
+ " Android build platform: %s\n"
+ " Install to device: %s\n",
+ qPrintable(options.inputFileName),
+ qPrintable(options.outputDirectory),
+ qPrintable(options.applicationBinary),
+ qPrintable(options.androidPlatform),
+ options.installApk
+ ? (options.installLocation.isEmpty() ? "Default device" : qPrintable(options.installLocation))
+ : "No"
+ );
+
+ if (options.build) {
+ if (options.gradle)
+ cleanAndroidFiles(options);
+ if (Q_UNLIKELY(options.timing))
+ fprintf(stdout, "[TIMING] %d ms: Cleaned Android file\n", options.timer.elapsed());
+
+ if (!copyAndroidTemplate(options))
+ return CannotCopyAndroidTemplate;
+
+ if (Q_UNLIKELY(options.timing))
+ fprintf(stdout, "[TIMING] %d ms: Copied Android template\n", options.timer.elapsed());
+ }
+
+ if (!readDependencies(&options))
+ return CannotReadDependencies;
+
+ if (Q_UNLIKELY(options.timing))
+ fprintf(stdout, "[TIMING] %d ms: Read dependencies\n", options.timer.elapsed());
+
+ if (options.deploymentMechanism != Options::Ministro && !copyStdCpp(&options))
+ return CannotCopyGnuStl;
+
+ if (Q_UNLIKELY(options.timing))
+ fprintf(stdout, "[TIMING] %d ms: Copied GNU STL\n", options.timer.elapsed());
+
+ if (!copyQtFiles(&options))
+ return CannotCopyQtFiles;
+
+ if (options.build) {
+ if (Q_UNLIKELY(options.timing))
+ fprintf(stdout, "[TIMING] %d ms: Copied Qt files\n", options.timer.elapsed());
+
+ if (!containsApplicationBinary(options))
+ return CannotFindApplicationBinary;
+
+ if (Q_UNLIKELY(options.timing))
+ fprintf(stdout, "[TIMING] %d ms: Checked for application binary\n", options.timer.elapsed());
+
+ bool needToCopyGdbServer = options.gdbServer == Options::True
+ || (options.gdbServer == Options::Auto && !options.releasePackage);
+ if (needToCopyGdbServer && !copyGdbServer(options))
+ return CannotCopyGdbServer;
+
+ if (Q_UNLIKELY(options.timing))
+ fprintf(stdout, "[TIMING] %d ms: Copied GDB server\n", options.timer.elapsed());
+
+ if (!copyAndroidExtraLibs(options))
+ return CannotCopyAndroidExtraLibs;
+
+ if (Q_UNLIKELY(options.timing))
+ fprintf(stdout, "[TIMING] %d ms: Copied extra libs\n", options.timer.elapsed());
+
+ if (!copyAndroidExtraResources(options))
+ return CannotCopyAndroidExtraResources;
+
+ if (!copyAndroidSources(options))
+ return CannotCopyAndroidSources;
+
+ if (Q_UNLIKELY(options.timing))
+ fprintf(stdout, "[TIMING] %d ms: Copied android sources\n", options.timer.elapsed());
+
+ if (!stripLibraries(options))
+ return CannotStripLibraries;
+
+ if (Q_UNLIKELY(options.timing))
+ fprintf(stdout, "[TIMING] %d ms: Stripped libraries\n", options.timer.elapsed());
+
+ if (!updateAndroidFiles(options))
+ return CannotUpdateAndroidFiles;
+
+ if (options.generateAssetsFileList && !generateAssetsFileList(options))
+ return CannotGenerateAssetsFileList;
+
+ if (Q_UNLIKELY(options.timing))
+ fprintf(stdout, "[TIMING] %d ms: Updated files\n", options.timer.elapsed());
+
+ if (!options.gradle && !createAndroidProject(options))
+ return CannotCreateAndroidProject;
+
+ if (Q_UNLIKELY(options.timing))
+ fprintf(stdout, "[TIMING] %d ms: Created project\n", options.timer.elapsed());
+
+ if (!buildAndroidProject(options))
+ return CannotBuildAndroidProject;
+
+ if (Q_UNLIKELY(options.timing))
+ fprintf(stdout, "[TIMING] %d ms: Built project\n", options.timer.elapsed());
+
+ if (!options.keyStore.isEmpty() && !signPackage(options))
+ return CannotSignPackage;
+
+ if (Q_UNLIKELY(options.timing))
+ fprintf(stdout, "[TIMING] %d ms: Signed package\n", options.timer.elapsed());
+ }
+
+ if (options.installApk && !installApk(options))
+ return CannotInstallApk;
+
+ if (Q_UNLIKELY(options.timing))
+ fprintf(stdout, "[TIMING] %d ms: Installed APK\n", options.timer.elapsed());
+
+ fprintf(stdout, "Android package built successfully in %.3f ms.\n", options.timer.elapsed() / 1000.);
+
+ if (options.installApk)
+ fprintf(stdout, " -- It can now be run from the selected device/emulator.\n");
+
+ fprintf(stdout, " -- File: %s\n", qPrintable(apkPath(options, options.keyStore.isEmpty() ? UnsignedAPK
+ : SignedAPK)));
+ fflush(stdout);
+ return 0;
+}