/**************************************************************************** ** ** 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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) , qmlImportScannerBinaryPath() {} 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 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 architectures; QString currentArchitecture; QString toolchainPrefix; QString ndkHost; bool buildAAB = false; // Package information DeploymentMechanism deploymentMechanism; QString packageName; QStringList extraLibs; QHash archExtraLibs; QStringList extraPlugins; QHash 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 BundledFile; QHash> bundledFiles; QHash> qtDependencies; QHash localLibs; bool usesOpenGL = false; // Per package collected information QStringList localJars; QStringList initClasses; QStringList permissions; QStringList features; // Override qml import scanner path QString qmlImportScannerBinaryPath; }; static const QHash 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()) { 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; } else if (argument.compare(QLatin1String("--qml-importscanner-binary"), Qt::CaseInsensitive) == 0) { options.qmlImportScannerBinaryPath = arguments.at(++i).trimmed(); } } 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 [options]\n" "\n" " Creates an Android package in the build directory and\n" " builds it into an .apk file.\n\n" " Optional arguments:\n" " --input : Reads 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 : 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 : 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 : 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 : Keystore password.\n" " --storetype : Keystore type.\n" " --keypass : Password for private key (if different\n" " from keystore password.)\n" " --sigfile : Name of .SF/.DSA file.\n" " --digestalg : Name of digest algorithm. Default is\n" " \"SHA1\".\n" " --sigalg : Name of signature algorithm. Default is\n" " \"SHA1withRSA\".\n" " --tsa : Location of the Time Stamping Authority.\n" " --tsacert : 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 : 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 built apk.\n" " --qml-importscanner-binary : Override the\n" " default qmlimportscanner binary path. By default the\n" " qmlimportscanner binary is located using the Qt directory\n" " specified in the input file.\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 qmlImportScannerBinaryPath = jsonObject.value(QLatin1String("qml-importscanner-binary")); if (!qmlImportScannerBinaryPath.isUndefined()) options->qmlImportScannerBinaryPath = qmlImportScannerBinaryPath.toString(); } { 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 &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::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(" %1;%2\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(" %1;%2\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(" %1;%2\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(" %1;%2\n").arg(it.key(), localLibs.join(QLatin1Char(':'))); } QHash replacements; replacements[QStringLiteral("")] += qtLibs.trimmed(); replacements[QStringLiteral("")] = allLocalLibs.trimmed(); replacements[QStringLiteral("")] = 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 replacements; replacements[QStringLiteral("")] = 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("") .append(options.applicationBinary.toLatin1()) .append("\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 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(" \n").arg(permission); replacements[QStringLiteral("")] = permissions.trimmed(); QString features; for (const QString &feature : qAsConst(options.features)) features += QLatin1String(" \n").arg(feature); if (options.usesOpenGL) features += QLatin1String(" "); replacements[QStringLiteral("")] = 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 findFilesRecursively(const Options &options, const QFileInfo &info, const QString &rootPath) { if (!info.exists()) return QList(); if (info.isDir()) { QList 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(info.absoluteFilePath().mid(rootPath.length()), info.absoluteFilePath()); } } QList 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 *usedDependencies, QSet *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 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; ilocalLibs.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 *usedDependencies, QSet *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 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 *usedDependencies) { if (options->verbose) fprintf(stdout, "Scanning for QML imports.\n"); QString qmlImportScanner; if (!options->qmlImportScannerBinaryPath.isEmpty()) { qmlImportScanner = options->qmlImportScannerBinaryPath; } else { 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; iverbose) 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 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 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 usedDependencies; QSet 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::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 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 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 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; }