blob: 5ac22287e2d5f563bbccac4380131571182be6e9 [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2019 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>
#ifdef Q_CC_MSVC
#define popen _popen
#define QT_POPEN_READ "rb"
#define pclose _pclose
#else
#define QT_POPEN_READ "r"
#endif
class ActionTimer
{
qint64 started;
public:
ActionTimer() = default;
void start()
{
started = QDateTime::currentMSecsSinceEpoch();
}
int elapsed()
{
return int(QDateTime::currentMSecsSinceEpoch() - started);
}
};
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
const QString& processedCommand = command;
#endif
return popen(processedCommand.toLocal8Bit().constData(), QT_POPEN_READ);
}
struct QtDependency
{
QtDependency(const QString &rpath, const 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)
, build(true)
, auxMode(false)
, deploymentMechanism(Bundled)
, releasePackage(false)
, digestAlg(QLatin1String("SHA-256"))
, sigAlg(QLatin1String("SHA256withRSA"))
, internalSf(false)
, sectionsOnly(false)
, protectedAuthenticationPath(false)
, jarSigner(false)
, installApk(false)
, uninstallApk(false)
{}
enum DeploymentMechanism
{
Bundled,
Ministro
};
enum TriState {
Auto,
False,
True
};
bool helpRequested;
bool verbose;
bool timing;
bool build;
bool auxMode;
ActionTimer timer;
// External tools
QString sdkPath;
QString sdkBuildToolsVersion;
QString ndkPath;
QString jdkPath;
// Build paths
QString qtInstallDirectory;
std::vector<QString> extraPrefixDirs;
QString androidSourceDirectory;
QString outputDirectory;
QString inputFileName;
QString applicationBinary;
QString rootPath;
QStringList qmlImportPaths;
QStringList qrcFiles;
// Versioning
QString versionName;
QString versionCode;
// lib c++ path
QString stdCppPath;
QString stdCppName = QStringLiteral("c++_shared");
// Build information
QString androidPlatform;
QHash<QString, QString> architectures;
QString currentArchitecture;
QString toolchainPrefix;
QString ndkHost;
bool buildAAB = false;
// Package information
DeploymentMechanism deploymentMechanism;
QString packageName;
QStringList extraLibs;
QHash<QString, QStringList> archExtraLibs;
QStringList extraPlugins;
QHash<QString, QStringList> archExtraPlugins;
// 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;
QString apkPath;
// Installation information
bool installApk;
bool uninstallApk;
QString installLocation;
// Per architecture collected information
void clear(const QString &arch)
{
currentArchitecture = arch;
}
typedef QPair<QString, QString> BundledFile;
QHash<QString, QList<BundledFile>> bundledFiles;
QHash<QString, QList<QtDependency>> qtDependencies;
QHash<QString, QStringList> localLibs;
bool usesOpenGL = false;
// Per package collected information
QStringList localJars;
QStringList initClasses;
QStringList permissions;
QStringList features;
};
static const QHash<QByteArray, QByteArray> elfArchitecures = {
{"aarch64", "arm64-v8a"},
{"arm", "armeabi-v7a"},
{"i386", "x86"},
{"x86_64", "x86_64"}
};
// 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 QLatin1String("\"\"");
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 QLatin1String("\"\"");
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);
}
QString architecureFromName(const QString &name)
{
QRegExp architecture(QStringLiteral(".*_(armeabi-v7a|arm64-v8a|x86|x86_64).so"));
if (!architecture.exactMatch(name))
return {};
return architecture.capturedTexts().last();
}
QString fileArchitecture(const Options &options, const QString &path)
{
auto arch = architecureFromName(path);
if (!arch.isEmpty())
return arch;
QString readElf = QLatin1String("%1/toolchains/%2/prebuilt/%3/bin/llvm-readobj").arg(options.ndkPath,
options.toolchainPrefix,
options.ndkHost);
#if defined(Q_OS_WIN32)
readElf += QLatin1String(".exe");
#endif
if (!QFile::exists(readElf)) {
fprintf(stderr, "Command does not exist: %s\n", qPrintable(readElf));
return {};
}
readElf = QLatin1String("%1 -needed-libs %2").arg(shellQuote(readElf), shellQuote(path));
FILE *readElfCommand = openProcess(readElf);
if (!readElfCommand) {
fprintf(stderr, "Cannot execute command %s\n", qPrintable(readElf));
return {};
}
char buffer[512];
while (fgets(buffer, sizeof(buffer), readElfCommand) != nullptr) {
QByteArray line = QByteArray::fromRawData(buffer, qstrlen(buffer));
QString library;
line = line.trimmed();
if (line.startsWith("Arch: ")) {
auto it = elfArchitecures.find(line.mid(6));
pclose(readElfCommand);
return it != elfArchitecures.constEnd() ? QString::fromLatin1(it.value()) : QString{};
}
}
pclose(readElfCommand);
return {};
}
bool checkArchitecture(const Options &options, const QString &fileName)
{
return fileArchitecture(options, fileName) == options.currentArchitecture;
}
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("--aab"), Qt::CaseInsensitive) == 0) {
options.buildAAB = true;
options.build = true;
options.jarSigner = true;
} else if (!options.buildAAB && 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("--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("--jdk"), Qt::CaseInsensitive) == 0) {
if (i + 1 == arguments.size())
options.helpRequested = true;
else
options.jdkPath = arguments.at(++i);
} else if (argument.compare(QLatin1String("--apk"), Qt::CaseInsensitive) == 0) {
if (i + 1 == arguments.size())
options.helpRequested = true;
else
options.apkPath = 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("--aux-mode"), Qt::CaseInsensitive) == 0) {
options.auxMode = true;
}
}
if (options.inputFileName.isEmpty())
options.inputFileName = QLatin1String("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"
" --aab: Build an Android App Bundle.\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"
" --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"
" --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"
" --aux-mode: Operate in auxiliary mode. This will only copy the\n"
" dependencies into the build directory and update the XML templates.\n"
" The project will not be built or installed.\n"
" --apk <path/where/to/copy/the/apk>: Path where to copy the built apk.\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,
const Options &options,
bool forceOverwrite = false)
{
if (QFile::exists(destinationFileName)) {
QFileInfo destinationFileInfo(destinationFileName);
QFileInfo sourceFileInfo(sourceFileName);
if (!forceOverwrite
&& sourceFileInfo.lastModified() <= destinationFileInfo.lastModified()
&& !alwaysOverwritableFile(destinationFileName)) {
if (options.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 (options.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 {};
}
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 = QDir::fromNativeSeparators(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(QLatin1String("sdkBuildToolsRevision"));
if (!value.isUndefined())
options->sdkBuildToolsVersion = value.toString();
}
{
const QJsonValue qtInstallDirectory = jsonObject.value(QLatin1String("qt"));
if (qtInstallDirectory.isUndefined()) {
fprintf(stderr, "No Qt directory in json file %s\n", qPrintable(options->inputFileName));
return false;
}
options->qtInstallDirectory = qtInstallDirectory.toString();
}
{
const auto extraPrefixDirs = jsonObject.value(QLatin1String("extraPrefixDirs")).toArray();
options->extraPrefixDirs.reserve(extraPrefixDirs.size());
for (const auto &prefix : extraPrefixDirs) {
options->extraPrefixDirs.push_back(prefix.toString());
}
}
{
const QJsonValue androidSourcesDirectory = jsonObject.value(QLatin1String("android-package-source-directory"));
if (!androidSourcesDirectory.isUndefined())
options->androidSourceDirectory = androidSourcesDirectory.toString();
}
{
const QJsonValue androidVersionName = jsonObject.value(QLatin1String("android-version-name"));
if (!androidVersionName.isUndefined())
options->versionName = androidVersionName.toString();
else
options->versionName = QStringLiteral("1.0");
}
{
const QJsonValue androidVersionCode = jsonObject.value(QLatin1String("android-version-code"));
if (!androidVersionCode.isUndefined())
options->versionCode = androidVersionCode.toString();
else
options->versionCode = QStringLiteral("1");
}
{
const QJsonObject targetArchitectures = jsonObject.value(QLatin1String("architectures")).toObject();
if (targetArchitectures.isEmpty()) {
fprintf(stderr, "No target architecture defined in json file.\n");
return false;
}
for (auto it = targetArchitectures.constBegin(); it != targetArchitectures.constEnd(); ++it) {
if (it.value().isUndefined()) {
fprintf(stderr, "Invalid architecure.\n");
return false;
}
if (it.value().isNull())
continue;
options->architectures.insert(it.key(), it.value().toString());
}
}
{
const QJsonValue ndk = jsonObject.value(QLatin1String("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(QLatin1String("toolchain-prefix"));
if (toolchainPrefix.isUndefined()) {
fprintf(stderr, "No toolchain prefix defined in json file.\n");
return false;
}
options->toolchainPrefix = toolchainPrefix.toString();
}
{
const QJsonValue ndkHost = jsonObject.value(QLatin1String("ndk-host"));
if (ndkHost.isUndefined()) {
fprintf(stderr, "No NDK host defined in json file.\n");
return false;
}
options->ndkHost = ndkHost.toString();
}
{
const QJsonValue extraLibs = jsonObject.value(QLatin1String("android-extra-libs"));
if (!extraLibs.isUndefined())
options->extraLibs = extraLibs.toString().split(QLatin1Char(','), QString::SkipEmptyParts);
}
{
const QJsonValue extraPlugins = jsonObject.value(QLatin1String("android-extra-plugins"));
if (!extraPlugins.isUndefined())
options->extraPlugins = extraPlugins.toString().split(QLatin1Char(','));
}
{
const QJsonValue stdcppPath = jsonObject.value(QLatin1String("stdcpp-path"));
if (stdcppPath.isUndefined()) {
fprintf(stderr, "No stdcpp-path defined in json file.\n");
return false;
}
options->stdCppPath = stdcppPath.toString();
}
{
const QJsonValue qmlRootPath = jsonObject.value(QLatin1String("qml-root-path"));
if (!qmlRootPath.isUndefined())
options->rootPath = qmlRootPath.toString();
}
{
const QJsonValue qmlImportPaths = jsonObject.value(QLatin1String("qml-import-paths"));
if (!qmlImportPaths.isUndefined())
options->qmlImportPaths = qmlImportPaths.toString().split(QLatin1Char(','));
}
{
const QJsonValue applicationBinary = jsonObject.value(QLatin1String("application-binary"));
if (applicationBinary.isUndefined()) {
fprintf(stderr, "No application binary defined in json file.\n");
return false;
}
options->applicationBinary = applicationBinary.toString();
if (options->build) {
for (auto it = options->architectures.constBegin(); it != options->architectures.constEnd(); ++it) {
if (!QFile::exists(QLatin1String("%1/libs/%2/lib%3_%2.so").arg(options->outputDirectory, it.key(), options->applicationBinary))) {
fprintf(stderr, "Cannot find application binary %s.\n", qPrintable(options->applicationBinary));
return false;
}
}
}
}
{
const QJsonValue deploymentDependencies = jsonObject.value(QLatin1String("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();
auto arch = fileArchitecture(*options, subPath);
if (!arch.isEmpty()) {
options->qtDependencies[arch].append(QtDependency(subPath.mid(options->qtInstallDirectory.length() + 1),
subPath));
} else if (options->verbose) {
fprintf(stderr, "Skipping \"%s\", unknown architecture\n", qPrintable(subPath));
fflush(stderr);
}
}
}
} else {
auto arch = fileArchitecture(*options, path);
if (!arch.isEmpty()) {
options->qtDependencies[arch].append(QtDependency(dependency.toString(), path));
} else if (options->verbose) {
fprintf(stderr, "Skipping \"%s\", unknown architecture\n", qPrintable(path));
fflush(stderr);
}
}
}
}
}
{
const QJsonValue qrcFiles = jsonObject.value(QLatin1String("qrcFiles"));
options->qrcFiles = qrcFiles.toString().split(QLatin1Char(','), QString::SkipEmptyParts);
}
options->packageName = packageNameFromAndroidManifest(options->androidSourceDirectory + QLatin1String("/AndroidManifest.xml"));
if (options->packageName.isEmpty())
options->packageName = cleanPackageName(QLatin1String("org.qtproject.example.%1").arg(options->applicationBinary));
return true;
}
bool copyFiles(const QDir &sourceDirectory, const QDir &destinationDirectory, const Options &options, 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() + QLatin1Char('/') + dir.dirName()), options, forceOverwrite))
return false;
} else {
QString destination = destinationDirectory.absoluteFilePath(entry.fileName());
if (!copyFileIfNewer(entry.absoluteFilePath(), destination, options, 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);
}
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);
}
bool copyAndroidTemplate(const Options &options)
{
if (options.verbose)
fprintf(stdout, "Copying Android package template.\n");
if (!copyGradleTemplate(options))
return false;
if (!copyAndroidTemplate(options, QLatin1String("/src/android/templates")))
return false;
return true;
}
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, true);
}
bool copyAndroidExtraLibs(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 (!checkArchitecture(*options, extraLibInfo.filePath())) {
if (options->verbose)
fprintf(stdout, "Skipping \"%s\", architecture mismatch.\n", qPrintable(extraLib));
continue;
}
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->currentArchitecture
+ QLatin1Char('/')
+ extraLibInfo.fileName());
if (!copyFileIfNewer(extraLib, destinationFile, *options))
return false;
options->archExtraLibs[options->currentArchitecture] += extraLib;
}
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(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 + QLatin1String("/assets/") + resourceDir.dirName() + QLatin1Char('/');
QString libsDir = options->outputDirectory + QLatin1String("/libs/") + options->currentArchitecture + 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 {
if (!checkArchitecture(*options, originFile))
continue;
destinationFile = libsDir + resourceFile;
options->archExtraPlugins[options->currentArchitecture] += resourceFile;
}
if (!copyFileIfNewer(originFile, destinationFile, *options))
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(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 qtLibs;
QString allLocalLibs;
QString extraLibs;
for (auto it = options->architectures.constBegin(); it != options->architectures.constEnd(); ++it) {
QString libsPath = QLatin1String("libs/") + it.key() + QLatin1Char('/');
qtLibs += QLatin1String(" <item>%1;%2</item>\n").arg(it.key(), options->stdCppName);
for (const Options::BundledFile &bundledFile : options->bundledFiles[it.key()]) {
if (bundledFile.second.startsWith(QLatin1String("lib/"))) {
QString s = bundledFile.second.mid(sizeof("lib/lib") - 1);
s.chop(sizeof(".so") - 1);
qtLibs += QLatin1String(" <item>%1;%2</item>\n").arg(it.key(), s);
}
}
if (!options->archExtraLibs[it.key()].isEmpty()) {
for (const QString &extraLib : options->archExtraLibs[it.key()]) {
QFileInfo extraLibInfo(extraLib);
QString name = extraLibInfo.fileName().mid(sizeof("lib") - 1);
name.chop(sizeof(".so") - 1);
extraLibs += QLatin1String(" <item>%1;%2</item>\n").arg(it.key(), name);
}
}
QStringList localLibs;
localLibs = options->localLibs[it.key()];
// If .pro file overrides dependency detection, we need to see which platform plugin they picked
if (localLibs.isEmpty()) {
QString plugin;
for (const QtDependency &qtDependency : options->qtDependencies[it.key()]) {
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 (qtDependency.relativePath.contains(QLatin1String("libQt5OpenGL"))
|| qtDependency.relativePath.contains(QLatin1String("libQt5Quick"))) {
options->usesOpenGL |= true;
break;
}
}
if (plugin.isEmpty()) {
fflush(stdout);
fprintf(stderr, "No platform plugin, neither libqtforandroid.so or libqtforandroidGL.so, included in package. Please include one.\n");
fflush(stderr);
return false;
}
localLibs.append(plugin);
if (options->verbose)
fprintf(stdout, " -- Using platform plugin %s\n", qPrintable(plugin));
}
// remove all paths
for (auto &lib : localLibs) {
if (lib.endsWith(QLatin1String(".so")))
lib = lib.mid(lib.lastIndexOf(QLatin1Char('/')) + 1);
}
allLocalLibs += QLatin1String(" <item>%1;%2</item>\n").arg(it.key(), localLibs.join(QLatin1Char(':')));
}
QHash<QString, QString> replacements;
replacements[QStringLiteral("<!-- %%INSERT_QT_LIBS%% -->")] += qtLibs.trimmed();
replacements[QStringLiteral("<!-- %%INSERT_LOCAL_LIBS%% -->")] = allLocalLibs.trimmed();
replacements[QStringLiteral("<!-- %%INSERT_EXTRA_LIBS%% -->")] = extraLibs.trimmed();
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%% -->")] = options.applicationBinary;
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\" translatable=\"false\">")
.append(options.applicationBinary.toLatin1())
.append("</string></resources>\n"));
return true;
}
if (!updateFile(fileName, replacements))
return false;
return true;
}
bool updateAndroidManifest(Options &options)
{
if (options.verbose)
fprintf(stdout, " -- AndroidManifest.xml \n");
options.localJars.removeDuplicates();
options.initClasses.removeDuplicates();
QHash<QString, QString> replacements;
replacements[QStringLiteral("-- %%INSERT_APP_NAME%% --")] = options.applicationBinary;
replacements[QStringLiteral("-- %%INSERT_APP_LIB_NAME%% --")] = options.applicationBinary;
replacements[QStringLiteral("-- %%INSERT_LOCAL_JARS%% --")] = options.localJars.join(QLatin1Char(':'));
replacements[QStringLiteral("-- %%INSERT_INIT_CLASSES%% --")] = options.initClasses.join(QLatin1Char(':'));
replacements[QStringLiteral("-- %%INSERT_VERSION_NAME%% --")] = options.versionName;
replacements[QStringLiteral("-- %%INSERT_VERSION_CODE%% --")] = options.versionCode;
replacements[QStringLiteral("package=\"org.qtproject.example\"")] = QLatin1String("package=\"%1\"").arg(options.packageName);
replacements[QStringLiteral("-- %%BUNDLE_LOCAL_QT_LIBS%% --")]
= (options.deploymentMechanism == Options::Bundled) ? QLatin1String("1") : QLatin1String("0");
replacements[QStringLiteral("-- %%USE_LOCAL_QT_LIBS%% --")]
= (options.deploymentMechanism != Options::Ministro) ? QLatin1String("1") : QLatin1String("0");
QString permissions;
for (const QString &permission : qAsConst(options.permissions))
permissions += QLatin1String(" <uses-permission android:name=\"%1\" />\n").arg(permission);
replacements[QStringLiteral("<!-- %%INSERT_PERMISSIONS -->")] = permissions.trimmed();
QString features;
for (const QString &feature : qAsConst(options.features))
features += QLatin1String(" <uses-feature android:name=\"%1\" android:required=\"false\" />\n").arg(feature);
if (options.usesOpenGL)
features += QLatin1String(" <uses-feature android:glEsVersion=\"0x00020000\" android:required=\"true\" />");
replacements[QStringLiteral("<!-- %%INSERT_FEATURES -->")] = features.trimmed();
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() < 21) {
fprintf(stderr, "Invalid minSdkVersion version, minSdkVersion must be >= 21\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;
}
static QString absoluteFilePath(const Options *options, const QString &relativeFileName)
{
for (const auto &prefix : options->extraPrefixDirs) {
const QString path = prefix + QLatin1Char('/') + relativeFileName;
if (QFile::exists(path))
return path;
}
return options->qtInstallDirectory + QLatin1Char('/') + relativeFileName;
}
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)
{
for (const auto &prefix : options.extraPrefixDirs) {
QFileInfo info(prefix + QLatin1Char('/') + fileName);
if (info.exists())
return findFilesRecursively(options, info, prefix + QLatin1Char('/'));
}
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 = absoluteFilePath(options, QLatin1String("/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[options->currentArchitecture].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, absoluteFilePath(options, fileName));
if (!usedDependencies->contains(dependency.absolutePath)) {
options->qtDependencies[options->currentArchitecture].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[options->currentArchitecture].at(i) == replaces) {
options->localLibs[options->currentArchitecture][i] = fileName;
break;
}
}
} else if (!fileName.isEmpty()) {
options->localLibs[options->currentArchitecture].append(fileName);
}
if (fileName.endsWith(QLatin1String(".so")) && checkArchitecture(*options, fileName)) {
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));
}
options->permissions.removeDuplicates();
options->features.removeDuplicates();
return true;
}
QStringList getQtLibsFromElf(const Options &options, const QString &fileName)
{
QString readElf = QLatin1String("%1/toolchains/%2/prebuilt/%3/bin/llvm-readobj").arg(options.ndkPath,
options.toolchainPrefix,
options.ndkHost);
#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 = QLatin1String("%1 -needed-libs %2").arg(shellQuote(readElf), shellQuote(fileName));
FILE *readElfCommand = openProcess(readElf);
if (!readElfCommand) {
fprintf(stderr, "Cannot execute command %s\n", qPrintable(readElf));
return QStringList();
}
QStringList ret;
bool readLibs = false;
char buffer[512];
while (fgets(buffer, sizeof(buffer), readElfCommand) != nullptr) {
QByteArray line = QByteArray::fromRawData(buffer, qstrlen(buffer));
QString library;
line = line.trimmed();
if (!readLibs) {
if (line.startsWith("Arch: ")) {
auto it = elfArchitecures.find(line.mid(6));
if (it == elfArchitecures.constEnd() || *it != options.currentArchitecture.toLatin1()) {
if (options.verbose)
fprintf(stdout, "Skipping \"%s\", architecture mismatch\n", qPrintable(fileName));
return {};
}
}
readLibs = line.startsWith("NeededLibraries");
continue;
}
if (!line.startsWith("lib"))
continue;
library = QString::fromLatin1(line);
QString libraryName = QLatin1String("lib/") + library;
if (QFile::exists(absoluteFilePath(&options, 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 = absoluteFilePath(options, dependency);
usedDependencies->insert(dependency);
if (!readDependenciesFromElf(options,
absoluteDependencyPath,
usedDependencies,
remainingDependencies)) {
return false;
}
options->qtDependencies[options->currentArchitecture].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 (!options->qrcFiles.isEmpty()) {
qmlImportScanner += QLatin1String(" -qrcFiles");
for (const QString &qrcFile : options->qrcFiles)
qmlImportScanner += QLatin1Char(' ') + shellQuote(qrcFile);
}
if (rootPath.isEmpty())
rootPath = QFileInfo(options->inputFileName).absolutePath();
else
rootPath = QFileInfo(rootPath).absoluteFilePath();
if (!rootPath.endsWith(QLatin1Char('/')))
rootPath += QLatin1Char('/');
qmlImportScanner += QLatin1String(" -rootPath %1").arg(shellQuote(rootPath));
QStringList importPaths;
importPaths += shellQuote(options->qtInstallDirectory + QLatin1String("/qml"));
if (!rootPath.isEmpty())
importPaths += shellQuote(rootPath);
for (const QString &qmlImportPath : qAsConst(options->qmlImportPaths))
importPaths += shellQuote(qmlImportPath);
qmlImportScanner += QLatin1String(" -importPath %1").arg(importPaths.join(QLatin1Char(' ')));
if (options->verbose) {
fprintf(stdout, "Running qmlimportscanner with the following command: %s\n",
qmlImportScanner.toLocal8Bit().constData());
}
FILE *qmlImportScannerCommand = popen(qmlImportScanner.toLocal8Bit().constData(), QT_POPEN_READ);
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[options->currentArchitecture].append(fileName);
if (fileName.absolutePath.endsWith(QLatin1String(".so")) && checkArchitecture(*options, fileName.absolutePath)) {
QSet<QString> remainingDependencies;
if (!readDependenciesFromElf(options, fileName.absolutePath, usedDependencies, &remainingDependencies))
return false;
}
}
}
}
return true;
}
bool runCommand(const Options &options, const QString &command)
{
if (options.verbose)
fprintf(stdout, "Running command '%s'\n", qPrintable(command));
FILE *runCommand = openProcess(command);
if (runCommand == nullptr) {
fprintf(stderr, "Cannot run command '%s'\n", qPrintable(command));
return false;
}
char buffer[4096];
while (fgets(buffer, sizeof(buffer), runCommand) != nullptr) {
if (options.verbose)
fprintf(stdout, "%s", buffer);
}
pclose(runCommand);
fflush(stdout);
fflush(stderr);
return true;
}
bool createRcc(const Options &options)
{
auto assetsDir = QLatin1String("%1/assets").arg(options.outputDirectory);
if (!QDir{QLatin1String("%1/android_rcc_bundle").arg(assetsDir)}.exists()) {
fprintf(stdout, "Skipping createRCC\n");
return true;
}
if (options.verbose)
fprintf(stdout, "Create rcc bundle.\n");
QString rcc = options.qtInstallDirectory + QLatin1String("/bin/rcc");
#if defined(Q_OS_WIN32)
rcc += QLatin1String(".exe");
#endif
if (!QFile::exists(rcc)) {
fprintf(stderr, "rcc not found: %s\n", qPrintable(rcc));
return false;
}
auto currentDir = QDir::currentPath();
if (!QDir::setCurrent(QLatin1String("%1/android_rcc_bundle").arg(assetsDir))) {
fprintf(stderr, "Cannot set current dir to: %s\n", qPrintable(QLatin1String("%1/android_rcc_bundle").arg(assetsDir)));
return false;
}
bool res = runCommand(options, QLatin1String("%1 --project -o %2").arg(rcc, shellQuote(QLatin1String("%1/android_rcc_bundle.qrc").arg(assetsDir))));
if (!res)
return false;
QFile::rename(QLatin1String("%1/android_rcc_bundle.qrc").arg(assetsDir), QLatin1String("%1/android_rcc_bundle/android_rcc_bundle.qrc").arg(assetsDir));
res = runCommand(options, QLatin1String("%1 %2 --binary -o %3 android_rcc_bundle.qrc").arg(rcc, shellQuote(QLatin1String("--root=/android_rcc_bundle/")),
shellQuote(QLatin1String("%1/android_rcc_bundle.rcc").arg(assetsDir))));
if (!QDir::setCurrent(currentDir)) {
fprintf(stderr, "Cannot set current dir to: %s\n", qPrintable(currentDir));
return false;
}
QFile::remove(QLatin1String("%1/android_rcc_bundle.qrc").arg(assetsDir));
QDir{QLatin1String("%1/android_rcc_bundle").arg(assetsDir)}.removeRecursively();
return res;
}
bool readDependencies(Options *options)
{
if (options->verbose)
fprintf(stdout, "Detecting dependencies of application.\n");
// Override set in .pro file
if (!options->qtDependencies[options->currentArchitecture].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, QLatin1String("%1/libs/%2/lib%3_%2.so").arg(options->outputDirectory, options->currentArchitecture, options->applicationBinary), &usedDependencies, &remainingDependencies))
return false;
while (!remainingDependencies.isEmpty()) {
QSet<QString>::iterator start = remainingDependencies.begin();
QString fileName = absoluteFilePath(options, *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[options->currentArchitecture].begin();
while (it != options->localLibs[options->currentArchitecture].end()) {
QStringList unmetDependencies;
if (!goodToCopy(options, absoluteFilePath(options, *it), &unmetDependencies)) {
fprintf(stdout, "Skipping %s due to unmet dependencies: %s\n",
qPrintable(*it),
qPrintable(unmetDependencies.join(QLatin1Char(','))));
it = options->localLibs[options->currentArchitecture].erase(it);
} else {
++it;
}
}
if ((!options->rootPath.isEmpty() || options->qrcFiles.isEmpty()) &&
!scanImports(options, &usedDependencies))
return false;
return true;
}
bool containsApplicationBinary(Options *options)
{
if (!options->build)
return true;
if (options->verbose)
fprintf(stdout, "Checking if application binary is in package.\n");
QFileInfo applicationBinary(options->applicationBinary);
QString applicationFileName = QLatin1String("lib%1_%2.so").arg(options->applicationBinary,
options->currentArchitecture);
QString applicationPath = QLatin1String("%1/libs/%2/%3").arg(options->outputDirectory,
options->currentArchitecture,
applicationFileName);
if (!QFile::exists(applicationPath)) {
#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(applicationFileName),
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 = QLatin1String("%1%2 %3").arg(shellQuote(adb), installOption, 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;
if (!checkArchitecture(*options, file))
return false;
bool ret = true;
const auto libs = getQtLibsFromElf(*options, file);
for (const QString &lib : libs) {
if (!options->qtDependencies[options->currentArchitecture].contains(QtDependency(lib, absoluteFilePath(options, 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
auto assetsDestinationDirectory = QLatin1String("assets/android_rcc_bundle/");
for (const QtDependency &qtDependency : qAsConst(options->qtDependencies[options->currentArchitecture])) {
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 = qtDependency.relativePath.mid(qtDependency.relativePath.lastIndexOf(QLatin1Char('/')) + 1);
}
destinationFileName = libsDirectory + options->currentArchitecture + QLatin1Char('/') + 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)) {
if (unmetDependencies.isEmpty()) {
if (options->verbose) {
fprintf(stdout, " -- Skipping %s, architecture mismatch.\n",
qPrintable(sourceFileName));
}
} else {
if (unmetDependencies.isEmpty()) {
if (options->verbose) {
fprintf(stdout, " -- Skipping %s, architecture mismatch.\n",
qPrintable(sourceFileName));
}
} else {
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)) {
return false;
}
options->bundledFiles[options->currentArchitecture] += 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 = QLatin1String("%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 = QLatin1String("%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(), QT_POPEN_READ);
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();
}
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 buildAndroidProject(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 = QLatin1String("%1 --no-daemon %2").arg(shellQuote(gradlePath), options.releasePackage ? QLatin1String(" assembleRelease") : QLatin1String(" assembleDebug"));
if (options.buildAAB)
commandLine += QLatin1String(" bundle");
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 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 {
AAB,
UnsignedAPK,
SignedAPK
};
QString packagePath(const Options &options, PackageType pt)
{
QString path(options.outputDirectory);
path += QLatin1String("/build/outputs/%1/").arg(pt >= UnsignedAPK ? QStringLiteral("apk") : QStringLiteral("bundle"));
QString buildType(options.releasePackage ? QLatin1String("release/") : QLatin1String("debug/"));
if (QDir(path + buildType).exists())
path += buildType;
path += QDir(options.outputDirectory).dirName() + QLatin1Char('-');
if (options.releasePackage) {
path += QLatin1String("release-");
if (pt >= UnsignedAPK) {
if (pt == UnsignedAPK)
path += QLatin1String("un");
path += QLatin1String("signed.apk");
} else {
path.chop(1);
path += QLatin1String(".aab");
}
} else {
path += QLatin1String("debug");
if (pt >= UnsignedAPK) {
if (pt == SignedAPK)
path += QLatin1String("-signed");
path += QLatin1String(".apk");
} else {
path += QLatin1String(".aab");
}
}
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 ")
+ packagePath(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 copyPackage(const Options &options)
{
fflush(stdout);
auto from = packagePath(options, options.keyStore.isEmpty() ? UnsignedAPK : SignedAPK);
QFile::remove(options.apkPath);
return QFile::copy(from, options.apkPath);
}
bool copyStdCpp(Options *options)
{
if (options->verbose)
fprintf(stdout, "Copying STL library\n");
QString stdCppPath = QLatin1String("%1/%2/lib%3.so").arg(options->stdCppPath, options->architectures[options->currentArchitecture], options->stdCppName);
if (!QFile::exists(stdCppPath)) {
fprintf(stderr, "STL library does not exist at %s\n", qPrintable(stdCppPath));
fflush(stdout);
fflush(stderr);
return false;
}
const QString destinationFile = QLatin1String("%1/libs/%2/lib%3.so").arg(options->outputDirectory,
options->currentArchitecture,
options->stdCppName);
return copyFileIfNewer(stdCppPath, destinationFile, *options);
}
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 = QLatin1String("jarsigner.exe");
#else
QString jarSignerTool = QLatin1String("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 = QLatin1String("%1 -sigalg %2 -digestalg %3 -keystore %4")
.arg(shellQuote(jarSignerTool), shellQuote(options.sigAlg), shellQuote(options.digestAlg), shellQuote(options.keyStore));
if (!options.keyStorePassword.isEmpty())
jarSignerTool += QLatin1String(" -storepass %1").arg(shellQuote(options.keyStorePassword));
if (!options.storeType.isEmpty())
jarSignerTool += QLatin1String(" -storetype %1").arg(shellQuote(options.storeType));
if (!options.keyPass.isEmpty())
jarSignerTool += QLatin1String(" -keypass %1").arg(shellQuote(options.keyPass));
if (!options.sigFile.isEmpty())
jarSignerTool += QLatin1String(" -sigfile %1").arg(shellQuote(options.sigFile));
if (!options.signedJar.isEmpty())
jarSignerTool += QLatin1String(" -signedjar %1").arg(shellQuote(options.signedJar));
if (!options.tsaUrl.isEmpty())
jarSignerTool += QLatin1String(" -tsa %1").arg(shellQuote(options.tsaUrl));
if (!options.tsaCert.isEmpty())
jarSignerTool += QLatin1String(" -tsacert %1").arg(shellQuote(options.tsaCert));
if (options.internalSf)
jarSignerTool += QLatin1String(" -internalsf");
if (options.sectionsOnly)
jarSignerTool += QLatin1String(" -sectionsonly");
if (options.protectedAuthenticationPath)
jarSignerTool += QLatin1String(" -protected");
auto signPackage = [&](const QString &file) {
fprintf(stdout, "Signing file %s\n", qPrintable(file));
fflush(stdout);
auto command = jarSignerTool + QLatin1String(" %1 %2")
.arg(file)
.arg(shellQuote(options.keyStoreAlias));
FILE *jarSignerCommand = openProcess(command);
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;
}
return true;
};
if (!signPackage(packagePath(options, UnsignedAPK)))
return false;
if (options.buildAAB && !signPackage(packagePath(options, AAB)))
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 = QLatin1String("%1%2 -f 4 %3 %4")
.arg(shellQuote(zipAlignTool),
options.verbose ? QLatin1String(" -v") : QLatin1String(),
packagePath(options, UnsignedAPK),
packagePath(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;
}
return QFile::remove(packagePath(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 = QLatin1String("%1%2 -f 4 %3 %4")
.arg(shellQuote(zipAlignTool),
options.verbose ? QLatin1String(" -v") : QLatin1String(),
packagePath(options, UnsignedAPK),
packagePath(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 = QLatin1String("%1 sign --ks %2")
.arg(shellQuote(apksignerTool), shellQuote(options.keyStore));
if (!options.keyStorePassword.isEmpty())
apkSignerCommandLine += QLatin1String(" --ks-pass pass:%1").arg(shellQuote(options.keyStorePassword));
if (!options.keyStoreAlias.isEmpty())
apkSignerCommandLine += QLatin1String(" --ks-key-alias %1").arg(shellQuote(options.keyStoreAlias));
if (!options.keyPass.isEmpty())
apkSignerCommandLine += QLatin1String(" --key-pass pass:%1").arg(shellQuote(options.keyPass));
if (options.verbose)
apkSignerCommandLine += QLatin1String(" --verbose");
apkSignerCommandLine += QLatin1String(" %1")
.arg(packagePath(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 = QLatin1String("%1 verify --verbose %2")
.arg(shellQuote(apksignerTool), packagePath(options, SignedAPK));
// Verify the package and remove the unsigned apk
return apkSignerRunner() && QFile::remove(packagePath(options, UnsignedAPK));
}
enum ErrorCode
{
Success,
SyntaxErrorOrHelpRequested = 1,
CannotReadInputFile = 2,
CannotCopyAndroidTemplate = 3,
CannotReadDependencies = 4,
CannotCopyGnuStl = 5,
CannotCopyQtFiles = 6,
CannotFindApplicationBinary = 7,
CannotCopyAndroidExtraLibs = 10,
CannotCopyAndroidSources = 11,
CannotUpdateAndroidFiles = 12,
CannotCreateAndroidProject = 13,
CannotBuildAndroidProject = 14,
CannotSignPackage = 15,
CannotInstallApk = 16,
CannotCopyAndroidExtraResources = 19,
CannotCopyApk = 20,
CannotCreateRcc = 21
};
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 && !options.auxMode) {
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());
}
for (auto it = options.architectures.constBegin(); it != options.architectures.constEnd(); ++it) {
options.clear(it.key());
if (!readDependencies(&options))
return CannotReadDependencies;
if (Q_UNLIKELY(options.timing))
fprintf(stdout, "[TIMING] %d ms: Read dependencies\n", options.timer.elapsed());
if (!copyQtFiles(&options))
return CannotCopyQtFiles;
if (Q_UNLIKELY(options.timing))
fprintf(stdout, "[TIMING] %d ms: Copied Qt files\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 (Q_UNLIKELY(options.timing))
fprintf(stdout, "[TIMING] %d ms: Copied extra resources\n", options.timer.elapsed());
if (!options.auxMode) {
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 (!containsApplicationBinary(&options))
return CannotFindApplicationBinary;
if (Q_UNLIKELY(options.timing))
fprintf(stdout, "[TIMING] %d ms: Checked for application binary\n", options.timer.elapsed());
if (options.deploymentMechanism != Options::Ministro) {
if (Q_UNLIKELY(options.timing))
fprintf(stdout, "[TIMING] %d ms: Bundled Qt libs\n", options.timer.elapsed());
}
}
if (!createRcc(options))
return CannotCreateRcc;
if (options.auxMode) {
if (!updateAndroidFiles(options))
return CannotUpdateAndroidFiles;
return 0;
}
if (options.build) {
if (!copyAndroidSources(options))
return CannotCopyAndroidSources;
if (Q_UNLIKELY(options.timing))
fprintf(stdout, "[TIMING] %d ms: Copied android sources\n", options.timer.elapsed());
if (!updateAndroidFiles(options))
return CannotUpdateAndroidFiles;
if (Q_UNLIKELY(options.timing))
fprintf(stdout, "[TIMING] %d ms: Updated files\n", options.timer.elapsed());
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 (!options.apkPath.isEmpty() && !copyPackage(options))
return CannotCopyApk;
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(packagePath(options, options.keyStore.isEmpty() ? UnsignedAPK
: SignedAPK)));
fflush(stdout);
return 0;
}