diff options
author | Arttu Tarkiainen <arttu.tarkiainen@qt.io> | 2020-11-20 17:09:30 +0200 |
---|---|---|
committer | Arttu Tarkiainen <arttu.tarkiainen@qt.io> | 2020-12-02 10:25:18 +0000 |
commit | 885a41d1667ac8ccf59a4de76cb8449074a466ac (patch) | |
tree | 7e0909c5b06754f458c6b6e7d3a77a4ffd994be7 /src | |
parent | f053b9a627921b03529b4f797a97b582675fbe71 (diff) |
Tools: refactor to move general purpose functionality to installer lib
This makes it possible to utilize parts of our existing tooling in the
offline installer from online installer generation process.
Task-number: QTIFW-2048
Change-Id: I7ee605be75541cc83a3b6909089bda45f0835bcf
Reviewed-by: Katja Marttila <katja.marttila@qt.io>
Diffstat (limited to 'src')
-rw-r--r-- | src/libs/ifwtools/binarycreator.cpp | 830 | ||||
-rw-r--r-- | src/libs/ifwtools/binarycreator.h | 123 | ||||
-rw-r--r-- | src/libs/ifwtools/ifwtools.pri | 19 | ||||
-rw-r--r-- | src/libs/ifwtools/ifwtools_global.h | 44 | ||||
-rw-r--r-- | src/libs/ifwtools/rcc/qcorecmdlineargs_p.h | 59 | ||||
-rw-r--r-- | src/libs/ifwtools/rcc/rcc.cpp | 1038 | ||||
-rw-r--r-- | src/libs/ifwtools/rcc/rcc.h | 140 | ||||
-rw-r--r-- | src/libs/ifwtools/rcc/rccmain.cpp | 239 | ||||
-rw-r--r-- | src/libs/ifwtools/repositorygen.cpp | 966 | ||||
-rw-r--r-- | src/libs/ifwtools/repositorygen.h | 88 | ||||
-rw-r--r-- | src/libs/ifwtools/resources/copylibsintobundle.sh | 189 | ||||
-rw-r--r-- | src/libs/ifwtools/resources/default_icon_mac.icns | bin | 0 -> 118992 bytes | |||
-rw-r--r-- | src/libs/ifwtools/resources/ifwtools.qrc | 7 | ||||
-rw-r--r-- | src/libs/installer/installer.pro | 1 |
14 files changed, 3743 insertions, 0 deletions
diff --git a/src/libs/ifwtools/binarycreator.cpp b/src/libs/ifwtools/binarycreator.cpp new file mode 100644 index 000000000..b3119f5be --- /dev/null +++ b/src/libs/ifwtools/binarycreator.cpp @@ -0,0 +1,830 @@ +/************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Installer Framework. +** +** $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 "binarycreator.h" + +#include "qtpatch.h" +#include "repositorygen.h" +#include "binarycontent.h" +#include "errors.h" +#include "fileio.h" +#include "init.h" +#include "repository.h" +#include "settings.h" +#include "utils.h" + +#include <QDateTime> +#include <QDirIterator> +#include <QDomDocument> +#include <QProcess> +#include <QRegExp> +#include <QSettings> +#include <QTemporaryFile> +#include <QTemporaryDir> + +#include <iostream> + +#ifdef Q_OS_MACOS +#include <QtCore/QtEndian> +#include <QtCore/QFile> +#include <QtCore/QVersionNumber> + +#include <mach-o/fat.h> +#include <mach-o/loader.h> +#endif + +using namespace QInstaller; +using namespace QInstallerTools; + +#ifndef Q_OS_WIN +static void chmod755(const QString &absolutFilePath) +{ + QFile::setPermissions(absolutFilePath, QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner + | QFile::ReadGroup | QFile::ExeGroup | QFile::ReadOther | QFile::ExeOther); +} +#endif + +#ifdef Q_OS_MACOS +template <typename T = uint32_t> T readInt(QIODevice *ioDevice, bool *ok, + bool swap, bool peek = false) { + const auto bytes = peek + ? ioDevice->peek(sizeof(T)) + : ioDevice->read(sizeof(T)); + if (bytes.size() != sizeof(T)) { + if (ok) + *ok = false; + return T(); + } + if (ok) + *ok = true; + T n = *reinterpret_cast<const T *>(bytes.constData()); + return swap ? qbswap(n) : n; +} + +static QVersionNumber readMachOMinimumSystemVersion(QIODevice *device) +{ + bool ok; + std::vector<qint64> machoOffsets; + + qint64 pos = device->pos(); + uint32_t magic = readInt(device, &ok, false); + if (magic == FAT_MAGIC || magic == FAT_CIGAM || + magic == FAT_MAGIC_64 || magic == FAT_CIGAM_64) { + bool swap = magic == FAT_CIGAM || magic == FAT_CIGAM_64; + uint32_t nfat = readInt(device, &ok, swap); + if (!ok) + return QVersionNumber(); + + for (uint32_t n = 0; n < nfat; ++n) { + const bool is64bit = magic == FAT_MAGIC_64 || magic == FAT_CIGAM_64; + fat_arch_64 fat_arch; + fat_arch.cputype = static_cast<cpu_type_t>(readInt(device, &ok, swap)); + if (!ok) + return QVersionNumber(); + + fat_arch.cpusubtype = static_cast<cpu_subtype_t>(readInt(device, &ok, swap)); + if (!ok) + return QVersionNumber(); + + fat_arch.offset = is64bit + ? readInt<uint64_t>(device, &ok, swap) : readInt(device, &ok, swap); + if (!ok) + return QVersionNumber(); + + fat_arch.size = is64bit + ? readInt<uint64_t>(device, &ok, swap) : readInt(device, &ok, swap); + if (!ok) + return QVersionNumber(); + + fat_arch.align = readInt(device, &ok, swap); + if (!ok) + return QVersionNumber(); + + fat_arch.reserved = is64bit ? readInt(device, &ok, swap) : 0; + if (!ok) + return QVersionNumber(); + + machoOffsets.push_back(static_cast<qint64>(fat_arch.offset)); + } + } else if (!ok) { + return QVersionNumber(); + } + + // Wasn't a fat file, so we just read a thin Mach-O from the original offset + if (machoOffsets.empty()) + machoOffsets.push_back(pos); + + std::vector<QVersionNumber> versions; + + for (const auto &offset : machoOffsets) { + if (!device->seek(offset)) + return QVersionNumber(); + + bool swap = false; + mach_header_64 header; + header.magic = readInt(device, nullptr, swap); + switch (header.magic) { + case MH_CIGAM: + case MH_CIGAM_64: + swap = true; + break; + case MH_MAGIC: + case MH_MAGIC_64: + break; + default: + return QVersionNumber(); + } + + header.cputype = static_cast<cpu_type_t>(readInt(device, &ok, swap)); + if (!ok) + return QVersionNumber(); + + header.cpusubtype = static_cast<cpu_subtype_t>(readInt(device, &ok, swap)); + if (!ok) + return QVersionNumber(); + + header.filetype = readInt(device, &ok, swap); + if (!ok) + return QVersionNumber(); + + header.ncmds = readInt(device, &ok, swap); + if (!ok) + return QVersionNumber(); + + header.sizeofcmds = readInt(device, &ok, swap); + if (!ok) + return QVersionNumber(); + + header.flags = readInt(device, &ok, swap); + if (!ok) + return QVersionNumber(); + + header.reserved = header.magic == MH_MAGIC_64 || header.magic == MH_CIGAM_64 + ? readInt(device, &ok, swap) : 0; + if (!ok) + return QVersionNumber(); + + for (uint32_t i = 0; i < header.ncmds; ++i) { + const uint32_t cmd = readInt(device, nullptr, swap); + const uint32_t cmdsize = readInt(device, nullptr, swap); + if (cmd == 0 || cmdsize == 0) + return QVersionNumber(); + + switch (cmd) { + case LC_VERSION_MIN_MACOSX: + case LC_VERSION_MIN_IPHONEOS: + case LC_VERSION_MIN_TVOS: + case LC_VERSION_MIN_WATCHOS: + const uint32_t version = readInt(device, &ok, swap, true); + if (!ok) + return QVersionNumber(); + + versions.push_back(QVersionNumber( + static_cast<int>(version >> 16), + static_cast<int>((version >> 8) & 0xff), + static_cast<int>(version & 0xff))); + break; + } + + const qint64 remaining = static_cast<qint64>(cmdsize - sizeof(cmd) - sizeof(cmdsize)); + if (device->read(remaining).size() != remaining) + return QVersionNumber(); + } + } + + std::sort(versions.begin(), versions.end()); + return !versions.empty() ? versions.front() : QVersionNumber(); +} +#endif + +static int assemble(Input input, const QInstaller::Settings &settings, const QString &signingIdentity) +{ +#ifdef Q_OS_MACOS + if (QInstaller::isInBundle(input.installerExePath)) { + const QString bundle = input.installerExePath; + // if the input file was a bundle + const QSettings s(QString::fromLatin1("%1/Contents/Info.plist").arg(input.installerExePath), + QSettings::NativeFormat); + input.installerExePath = QString::fromLatin1("%1/Contents/MacOS/%2").arg(bundle) + .arg(s.value(QLatin1String("CFBundleExecutable"), + QFileInfo(input.installerExePath).completeBaseName()).toString()); + } + + const bool createDMG = input.outputPath.endsWith(QLatin1String(".dmg")); + if (createDMG) + input.outputPath.replace(input.outputPath.length() - 4, 4, QLatin1String(".app")); + + const bool isBundle = input.outputPath.endsWith(QLatin1String(".app")); + const QString bundle = isBundle ? input.outputPath : QString(); + const BundleBackup bundleBackup(bundle); + + if (isBundle) { + // output should be a bundle + const QFileInfo fi(input.outputPath); + + QString minimumSystemVersion; + QFile file(input.installerExePath); + if (file.open(QIODevice::ReadOnly)) + minimumSystemVersion = readMachOMinimumSystemVersion(&file).normalized().toString(); + + const QString contentsResourcesPath = fi.filePath() + QLatin1String("/Contents/Resources/"); + + QInstaller::mkpath(fi.filePath() + QLatin1String("/Contents/MacOS")); + QInstaller::mkpath(contentsResourcesPath); + + { + QFile pkgInfo(fi.filePath() + QLatin1String("/Contents/PkgInfo")); + pkgInfo.open(QIODevice::WriteOnly); + QTextStream pkgInfoStream(&pkgInfo); + pkgInfoStream << QLatin1String("APPL????") << endl; + } + + QString iconFile; + if (QFile::exists(settings.installerApplicationIcon())) { + iconFile = settings.installerApplicationIcon(); + } else { + iconFile = QString::fromLatin1(":/resources/default_icon_mac.icns"); + } + + const QString iconTargetFile = fi.completeBaseName() + QLatin1String(".icns"); + QFile::copy(iconFile, contentsResourcesPath + iconTargetFile); + if (QDir(qApp->applicationDirPath() + QLatin1String("/qt_menu.nib")).exists()) { + copyDirectoryContents(qApp->applicationDirPath() + QLatin1String("/qt_menu.nib"), + contentsResourcesPath + QLatin1String("/qt_menu.nib")); + } + + QFile infoPList(fi.filePath() + QLatin1String("/Contents/Info.plist")); + infoPList.open(QIODevice::WriteOnly); + QTextStream plistStream(&infoPList); + plistStream << QLatin1String("<?xml version=\"1.0\" encoding=\"UTF-8\"?>") << endl; + plistStream << QLatin1String("<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" " + "\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">") << endl; + plistStream << QLatin1String("<plist version=\"1.0\">") << endl; + plistStream << QLatin1String("<dict>") << endl; + plistStream << QLatin1String("\t<key>CFBundleIconFile</key>") << endl; + plistStream << QLatin1String("\t<string>") << iconTargetFile << QLatin1String("</string>") + << endl; + plistStream << QLatin1String("\t<key>CFBundlePackageType</key>") << endl; + plistStream << QLatin1String("\t<string>APPL</string>") << endl; +#define QUOTE_(x) #x +#define QUOTE(x) QUOTE_(x) + plistStream << QLatin1String("\t<key>CFBundleShortVersionString</key>") << endl; + plistStream << QLatin1String("\t<string>") << QLatin1String(QUOTE(IFW_VERSION_STR)) << ("</string>") + << endl; + plistStream << QLatin1String("\t<key>CFBundleVersion</key>") << endl; + plistStream << QLatin1String("\t<string>") << QLatin1String(QUOTE(IFW_VERSION_STR)) << ("</string>") + << endl; +#undef QUOTE +#undef QUOTE_ + plistStream << QLatin1String("\t<key>CFBundleSignature</key>") << endl; + plistStream << QLatin1String("\t<string>\?\?\?\?</string>") << endl; + plistStream << QLatin1String("\t<key>CFBundleExecutable</key>") << endl; + plistStream << QLatin1String("\t<string>") << fi.completeBaseName() << QLatin1String("</string>") + << endl; + plistStream << QLatin1String("\t<key>CFBundleIdentifier</key>") << endl; + plistStream << QLatin1String("\t<string>com.yourcompany.installerbase</string>") << endl; + plistStream << QLatin1String("\t<key>NOTE</key>") << endl; + plistStream << QLatin1String("\t<string>This file was generated by Qt Installer Framework.</string>") + << endl; + plistStream << QLatin1String("\t<key>NSPrincipalClass</key>") << endl; + plistStream << QLatin1String("\t<string>NSApplication</string>") << endl; + if (!minimumSystemVersion.isEmpty()) { + plistStream << QLatin1String("\t<key>LSMinimumSystemVersion</key>") << endl; + plistStream << QLatin1String("\t<string>") << minimumSystemVersion << QLatin1String("</string>") << endl; + } + plistStream << QLatin1String("</dict>") << endl; + plistStream << QLatin1String("</plist>") << endl; + + input.outputPath = QString::fromLatin1("%1/Contents/MacOS/%2").arg(input.outputPath) + .arg(fi.completeBaseName()); + } +#elif defined(Q_OS_LINUX) + Q_UNUSED(settings) +#endif + + QTemporaryFile file(input.outputPath); + if (!file.open()) { + throw Error(QString::fromLatin1("Cannot copy %1 to %2: %3").arg(input.installerExePath, + input.outputPath, file.errorString())); + } + + const QString tempFile = file.fileName(); + file.close(); + file.remove(); + + QFile instExe(input.installerExePath); + if (!instExe.copy(tempFile)) { + throw Error(QString::fromLatin1("Cannot copy %1 to %2: %3").arg(instExe.fileName(), + tempFile, instExe.errorString())); + } + + QtPatch::patchBinaryFile(tempFile, QByteArray("MY_InstallerCreateDateTime_MY"), + QDateTime::currentDateTime().toString(QLatin1String("yyyy-MM-dd - HH:mm:ss")).toLatin1()); + + + input.installerExePath = tempFile; + +#if defined(Q_OS_WIN) + // setting the windows icon must happen before we append our binary data - otherwise they get lost :-/ + if (QFile::exists(settings.installerApplicationIcon())) { + // no error handling as this is not fatal + setApplicationIcon(tempFile, settings.installerApplicationIcon()); + } +#elif defined(Q_OS_MACOS) + if (isBundle) { + // no error handling as this is not fatal + const QString copyscript = QDir::temp().absoluteFilePath(QLatin1String("copylibsintobundle.sh")); + QFile::copy(QLatin1String(":/resources/copylibsintobundle.sh"), copyscript); + QFile::rename(tempFile, input.outputPath); + chmod755(copyscript); + QProcess p; + p.start(copyscript, QStringList() << bundle); + p.waitForFinished(-1); + QFile::rename(input.outputPath, tempFile); + QFile::remove(copyscript); + } +#endif + + QFile out(generateTemporaryFileName()); + + QString targetName = input.outputPath; +#ifdef Q_OS_MACOS + QDir resourcePath(QFileInfo(input.outputPath).dir()); + resourcePath.cdUp(); + resourcePath.cd(QLatin1String("Resources")); + targetName = resourcePath.filePath(QLatin1String("installer.dat")); +#endif + + { + QFile target(targetName); + if (target.exists() && !target.remove()) { + qCritical("Cannot remove target %s: %s", qPrintable(target.fileName()), + qPrintable(target.errorString())); + QFile::remove(tempFile); + return EXIT_FAILURE; + } + } + + try { + QInstaller::openForWrite(&out); + QFile exe(input.installerExePath); + +#ifdef Q_OS_MACOS + if (!exe.copy(input.outputPath)) { + throw Error(QString::fromLatin1("Cannot copy %1 to %2: %3").arg(exe.fileName(), + input.outputPath, exe.errorString())); + } +#else + QInstaller::openForRead(&exe); + QInstaller::appendData(&out, &exe, exe.size()); +#endif + + foreach (const QInstallerTools::PackageInfo &info, input.packages) { + QInstaller::ResourceCollection collection; + collection.setName(info.name.toUtf8()); + + qDebug() << "Creating resource archive for" << info.name; + foreach (const QString &file, info.copiedFiles) { + const QSharedPointer<Resource> resource(new Resource(file)); + qDebug().nospace() << "Appending " << file << " (" << humanReadableSize(resource->size()) << ")"; + collection.appendResource(resource); + } + input.manager.insertCollection(collection); + } + + const QList<QInstaller::OperationBlob> operations; + BinaryContent::writeBinaryContent(&out, operations, input.manager, + BinaryContent::MagicInstallerMarker, BinaryContent::MagicCookie); + } catch (const Error &e) { + qCritical("Error occurred while assembling the installer: %s", qPrintable(e.message())); + QFile::remove(tempFile); + return EXIT_FAILURE; + } + + if (!out.rename(targetName)) { + qCritical("Cannot write installer to %s: %s", targetName.toUtf8().constData(), + out.errorString().toUtf8().constData()); + QFile::remove(tempFile); + return EXIT_FAILURE; + } + +#ifndef Q_OS_WIN + chmod755(out.fileName()); +#endif + QFile::remove(tempFile); + +#ifdef Q_OS_MACOS + if (isBundle && !signingIdentity.isEmpty()) { + qDebug() << "Signing .app bundle..."; + + QProcess p; + p.start(QLatin1String("codesign"), + QStringList() << QLatin1String("--force") + << QLatin1String("--deep") + << QLatin1String("--sign") << signingIdentity + << bundle); + + if (!p.waitForFinished(-1)) { + qCritical("Failed to sign app bundle: error while running '%s %s': %s", + p.program().toUtf8().constData(), + p.arguments().join(QLatin1Char(' ')).toUtf8().constData(), + p.errorString().toUtf8().constData()); + return EXIT_FAILURE; + } + + if (p.exitStatus() == QProcess::NormalExit) { + if (p.exitCode() != 0) { + qCritical("Failed to sign app bundle: running codesign failed " + "with exit code %d: %s", p.exitCode(), + p.readAllStandardError().constData()); + return EXIT_FAILURE; + } + } + + qDebug() << "done."; + } + + bundleBackup.release(); + + if (createDMG) { + qDebug() << "creating a DMG disk image..."; + + const QString volumeName = QFileInfo(input.outputPath).fileName(); + const QString imagePath = QString::fromLatin1("%1/%2.dmg") + .arg(QFileInfo(bundle).path()) + .arg(volumeName); + + // no error handling as this is not fatal + QProcess p; + p.start(QLatin1String("/usr/bin/hdiutil"), + QStringList() << QLatin1String("create") + << imagePath + << QLatin1String("-srcfolder") + << bundle + << QLatin1String("-ov") + << QLatin1String("-volname") + << volumeName + << QLatin1String("-fs") + << QLatin1String("HFS+")); + qDebug() << "running " << p.program() << p.arguments(); + p.waitForFinished(-1); + qDebug() << "removing" << bundle; + QDir(bundle).removeRecursively(); + qDebug() << "done."; + } +#else + Q_UNUSED(signingIdentity) +#endif + return EXIT_SUCCESS; +} + +QT_BEGIN_NAMESPACE +int runRcc(int argc, char *argv[]); +QT_END_NAMESPACE + +static int runRcc(const QStringList &args) +{ + const int argc = args.count(); + QVector<char*> argv(argc, nullptr); + for (int i = 0; i < argc; ++i) + argv[i] = qstrdup(qPrintable(args[i])); + + // Note: this does not run the rcc provided by Qt, this one is using the compiled in binarycreator + // version. If it happens that resource mapping fails, we might need to adapt the code here... + const int result = runRcc(argc, argv.data()); + + foreach (char *arg, argv) + delete [] arg; + + return result; +} + +static QSharedPointer<QInstaller::Resource> createDefaultResourceFile(const QString &directory, + const QString &binaryName) +{ + QTemporaryFile projectFile(directory + QLatin1String("/rccprojectXXXXXX.qrc")); + if (!projectFile.open()) + throw Error(QString::fromLatin1("Cannot create temporary file for generated rcc project file")); + projectFile.close(); + + const WorkingDirectoryChange wd(directory); + const QString projectFileName = QFileInfo(projectFile.fileName()).absoluteFilePath(); + + // 1. create the .qrc file + if (runRcc(QStringList() << QLatin1String("rcc") << QLatin1String("-project") << QLatin1String("-o") + << projectFileName) != EXIT_SUCCESS) { + throw Error(QString::fromLatin1("Cannot create rcc project file.")); + } + + // 2. create the binary resource file from the .qrc file + if (runRcc(QStringList() << QLatin1String("rcc") << QLatin1String("-binary") << QLatin1String("-o") + << binaryName << projectFileName) != EXIT_SUCCESS) { + throw Error(QString::fromLatin1("Cannot compile rcc project file.")); + } + + return QSharedPointer<QInstaller::Resource>(new QInstaller::Resource(binaryName, binaryName + .toUtf8())); +} + +static +QList<QSharedPointer<QInstaller::Resource> > createBinaryResourceFiles(const QStringList &resources) +{ + QList<QSharedPointer<QInstaller::Resource> > result; + foreach (const QString &resource, resources) { + QFile file(resource); + if (file.exists()) { + const QString binaryName = generateTemporaryFileName(); + const QString fileName = QFileInfo(file.fileName()).absoluteFilePath(); + const int status = runRcc(QStringList() << QLatin1String("rcc") + << QLatin1String("-binary") << QLatin1String("-o") << binaryName << fileName); + if (status != EXIT_SUCCESS) + continue; + + result.append(QSharedPointer<QInstaller::Resource> (new QInstaller::Resource(binaryName, + binaryName.toUtf8()))); + } + } + return result; +} + +void QInstallerTools::copyConfigData(const QString &configFile, const QString &targetDir) +{ + qDebug() << "Begin to copy configuration file and data."; + + const QString sourceConfigFile = QFileInfo(configFile).absoluteFilePath(); + const QString targetConfigFile = targetDir + QLatin1String("/config.xml"); + QInstallerTools::copyWithException(sourceConfigFile, targetConfigFile, QLatin1String("configuration")); + + QFile configXml(targetConfigFile); + QInstaller::openForRead(&configXml); + + QDomDocument dom; + dom.setContent(&configXml); + configXml.close(); + + // iterate over all child elements, searching for relative file names + const QDomNodeList children = dom.documentElement().childNodes(); + const QString sourceConfigFilePath = QFileInfo(sourceConfigFile).absolutePath(); + for (int i = 0; i < children.count(); ++i) { + QDomElement domElement = children.at(i).toElement(); + if (domElement.isNull()) + continue; + + const QString tagName = domElement.tagName(); + const QString elementText = domElement.text(); + qDebug().noquote() << QString::fromLatin1("Read dom element: <%1>%2</%1>.").arg(tagName, elementText); + + if (tagName == QLatin1String("ProductImages")) { + const QDomNodeList childNodes = domElement.childNodes(); + for (int i = 0; i < childNodes.count(); ++i) { + const QDomElement childElement = childNodes.at(i).toElement(); + const QString childName = childElement.tagName(); + if (childName != QLatin1String("Image")) + continue; + + const QString targetFile = targetDir + QLatin1Char('/') + childElement.text(); + const QFileInfo childFileInfo = QFileInfo(sourceConfigFilePath, childElement.text()); + QInstallerTools::copyWithException(childFileInfo.absoluteFilePath(), targetFile, childName); + } + continue; + } + + QString newName = domElement.text().replace(QRegExp(QLatin1String("\\\\|/|\\.|:")), + QLatin1String("_")); + + QString targetFile; + QFileInfo elementFileInfo; + if (tagName == QLatin1String("Icon") || tagName == QLatin1String("InstallerApplicationIcon")) { +#if defined(Q_OS_MACOS) + const QString suffix = QLatin1String(".icns"); +#elif defined(Q_OS_WIN) + const QString suffix = QLatin1String(".ico"); +#else + const QString suffix = QLatin1String(".png"); +#endif + elementFileInfo = QFileInfo(sourceConfigFilePath, elementText + suffix); + targetFile = targetDir + QLatin1Char('/') + newName + suffix; + } else { + elementFileInfo = QFileInfo(sourceConfigFilePath, elementText); + const QString suffix = elementFileInfo.completeSuffix(); + if (!suffix.isEmpty()) + newName.append(QLatin1Char('.') + suffix); + targetFile = targetDir + QLatin1Char('/') + newName; + } + if (!elementFileInfo.exists() || elementFileInfo.isDir()) + continue; + + domElement.replaceChild(dom.createTextNode(newName), domElement.firstChild()); + QInstallerTools::copyWithException(elementFileInfo.absoluteFilePath(), targetFile, tagName); + } + + QInstaller::openForWrite(&configXml); + QTextStream stream(&configXml); + dom.save(stream, 4); + + qDebug() << "done.\n"; +} + +int QInstallerTools::createBinary(BinaryCreatorArgs args, QString &argumentError) +{ + // increase maximum numbers of file descriptors +#if defined (Q_OS_MACOS) + struct rlimit rl; + getrlimit(RLIMIT_NOFILE, &rl); + rl.rlim_cur = qMin(static_cast<rlim_t>(OPEN_MAX), rl.rlim_max); + setrlimit(RLIMIT_NOFILE, &rl); +#endif + QString suffix; +#ifdef Q_OS_WIN + suffix = QLatin1String(".exe"); + if (!args.target.endsWith(suffix)) + args.target = args.target + suffix; +#endif + + // Begin check arguments + foreach (const QString &packageDir, args.packagesDirectories) { + if (!QFileInfo(packageDir).exists()) { + argumentError = QString::fromLatin1("Error: Package directory not found at the specified location."); + return EXIT_FAILURE; + } + } + foreach (const QString &repositoryDir, args.repositoryDirectories) { + if (!QFileInfo(repositoryDir).exists()) { + argumentError = QString::fromLatin1("Error: Only local filesystem repositories now supported."); + return EXIT_FAILURE; + } + } + if (!args.filteredPackages.isEmpty() && args.onlineOnly) { + argumentError = QString::fromLatin1("Error: 'online-only' option cannot be used " + "in conjunction with the 'include' or 'exclude' option. An 'online-only' installer will never " + "contain any components apart from the root component."); + return EXIT_FAILURE; + } +#ifdef Q_OS_WIN + if (!args.templateBinary.endsWith(suffix)) + args.templateBinary = args.templateBinary + suffix; +#endif + if (!QFileInfo(args.templateBinary).exists()) { + argumentError = QString::fromLatin1("Error: Template not found at the specified location."); + return EXIT_FAILURE; + } + const QFileInfo fi(args.configFile); + if (!fi.exists()) { + argumentError = QString::fromLatin1("Error: Config file %1 not found at the " + "specified location.").arg(fi.absoluteFilePath()); + return EXIT_FAILURE; + } + if (!fi.isFile()) { + argumentError = QString::fromLatin1("Error: Configuration %1 is not a file.") + .arg(fi.absoluteFilePath()); + return EXIT_FAILURE; + } + if (!fi.isReadable()) { + argumentError = QString::fromLatin1("Error: Config file %1 is not readable.") + .arg(fi.absoluteFilePath()); + return EXIT_FAILURE; + } + if (args.onlineOnly && args.offlineOnly) { + argumentError = QString::fromLatin1("You cannot use --online-only and " + "--offline-only at the same time."); + return EXIT_FAILURE; + } + if (args.target.isEmpty() && !args.compileResource) { + argumentError = QString::fromLatin1("Error: Target parameter missing."); + return EXIT_FAILURE; + } + if (args.configFile.isEmpty()) { + argumentError = QString::fromLatin1("Error: No configuration file selected."); + return EXIT_FAILURE; + } + if (args.packagesDirectories.isEmpty() && args.repositoryDirectories.isEmpty()) { + argumentError = QString::fromLatin1("Error: Both Package directory and Repository parameters missing."); + return EXIT_FAILURE; + } + if (args.onlineOnly) { + args.filteredPackages.append(QLatin1String("X_fake_filter_component_for_online_only_installer_X")); + args.ftype = QInstallerTools::Include; + } + // End check arguments + qDebug() << "Parsed arguments, ok."; + + Input input; + int exitCode = EXIT_FAILURE; + QTemporaryDir tmp; + tmp.setAutoRemove(false); + const QString tmpMetaDir = tmp.path(); + QTemporaryDir tmp2; + tmp2.setAutoRemove(false); + const QString tmpRepoDir = tmp2.path(); + try { + const Settings settings = Settings::fromFileAndPrefix(args.configFile, QFileInfo(args.configFile) + .absolutePath()); + + // Note: the order here is important + + PackageInfoVector packages; + + // 1; update the list of available compressed packages + if (!args.repositoryDirectories.isEmpty()) { + // 1.1; search packages + PackageInfoVector precompressedPackages = createListOfRepositoryPackages(args.repositoryDirectories, + &args.filteredPackages, args.ftype); + // 1.2; add to common vector + packages.append(precompressedPackages); + } + + // 2; update the list of available prepared packages + if (!args.packagesDirectories.isEmpty()) { + // 2.1; search packages + PackageInfoVector preparedPackages = createListOfPackages(args.packagesDirectories, + &args.filteredPackages, args.ftype); + // 2.2; copy the packages data and setup the packages vector with the files we copied, + // must happen before copying meta data because files will be compressed if + // needed and meta data generation relies on this + copyComponentData(args.packagesDirectories, tmpRepoDir, &preparedPackages); + // 2.3; add to common vector + packages.append(preparedPackages); + } + + // 3; copy the meta data of the available packages, generate Updates.xml + copyMetaData(tmpMetaDir, tmpRepoDir, packages, settings + .applicationName(), settings.version(), QStringList()); + + // 4; copy the configuration file and and icons etc. + copyConfigData(args.configFile, tmpMetaDir + QLatin1String("/installer-config")); + { + QSettings confInternal(tmpMetaDir + QLatin1String("/config/config-internal.ini") + , QSettings::IniFormat); + // assume offline installer if there are no repositories and no + //--online-only not set + args.offlineOnly = args.offlineOnly | settings.repositories().isEmpty(); + if (args.onlineOnly) + args.offlineOnly = !args.onlineOnly; + confInternal.setValue(QLatin1String("offlineOnly"), args.offlineOnly); + } + +#ifdef Q_OS_MACOS + // on mac, we enforce building a bundle + if (!args.target.endsWith(QLatin1String(".app")) && !args.target.endsWith(QLatin1String(".dmg"))) + args.target += QLatin1String(".app"); +#endif + if (!args.compileResource) { + // 5; put the copied resources into a resource file + ResourceCollection metaCollection("QResources"); + metaCollection.appendResource(createDefaultResourceFile(tmpMetaDir, + generateTemporaryFileName())); + metaCollection.appendResources(createBinaryResourceFiles(args.resources)); + input.manager.insertCollection(metaCollection); + + input.packages = packages; + input.outputPath = args.target; + input.installerExePath = args.templateBinary; + + qDebug() << "Creating the binary"; + exitCode = assemble(input, settings, args.signingIdentity); + } else { + createDefaultResourceFile(tmpMetaDir, QDir::currentPath() + QLatin1String("/update.rcc")); + exitCode = EXIT_SUCCESS; + } + } catch (const Error &e) { + QFile::remove(input.outputPath); + std::cerr << "Caught exception: " << e.message() << std::endl; + } catch (...) { + QFile::remove(input.outputPath); + std::cerr << "Unknown exception caught" << std::endl; + } + + qDebug() << "Cleaning up..."; + const ResourceCollection collection = input.manager.collectionByName("QResources"); + foreach (const QSharedPointer<QInstaller::Resource> &resource, collection.resources()) + QFile::remove(QString::fromUtf8(resource->name())); + QInstaller::removeDirectory(tmpMetaDir, true); + QInstaller::removeDirectory(tmpRepoDir, true); + + return exitCode; +} diff --git a/src/libs/ifwtools/binarycreator.h b/src/libs/ifwtools/binarycreator.h new file mode 100644 index 000000000..c20e230a4 --- /dev/null +++ b/src/libs/ifwtools/binarycreator.h @@ -0,0 +1,123 @@ +/************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Installer Framework. +** +** $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$ +** +**************************************************************************/ + +#ifndef BINARYCREATOR_H +#define BINARYCREATOR_H + +#include "ifwtools_global.h" + +#include "repositorygen.h" +#include "fileutils.h" +#include "binaryformat.h" + +#include <QtCore/QString> +#include <QtCore/QFile> + +namespace QInstallerTools { + +struct Input +{ + QString outputPath; + QString installerExePath; + QInstallerTools::PackageInfoVector packages; + QInstaller::ResourceCollectionManager manager; +}; + +struct IFWTOOLS_EXPORT BinaryCreatorArgs +{ + QString target; + QString configFile; + QString templateBinary; + QStringList packagesDirectories; + QStringList repositoryDirectories; + bool onlineOnly = false; + bool offlineOnly = false; + QStringList resources; + QStringList filteredPackages; + FilterType ftype = QInstallerTools::Exclude; + bool compileResource = false; + QString signingIdentity; +}; + +class BundleBackup +{ +public: + explicit BundleBackup(const QString &bundle = QString()) + : bundle(bundle) + { + if (!bundle.isEmpty() && QFileInfo(bundle).exists()) { + backup = QInstaller::generateTemporaryFileName(bundle); + QFile::rename(bundle, backup); + } + } + + ~BundleBackup() + { + if (!backup.isEmpty()) { + QInstaller::removeDirectory(bundle); + QFile::rename(backup, bundle); + } + } + + void release() const + { + if (!backup.isEmpty()) + QInstaller::removeDirectory(backup); + backup.clear(); + } + +private: + const QString bundle; + mutable QString backup; +}; + +class WorkingDirectoryChange +{ +public: + explicit WorkingDirectoryChange(const QString &path) + : oldPath(QDir::currentPath()) + { + QDir::setCurrent(path); + } + + virtual ~WorkingDirectoryChange() + { + QDir::setCurrent(oldPath); + } + +private: + const QString oldPath; +}; + +void copyConfigData(const QString &configFile, const QString &targetDir); + +int IFWTOOLS_EXPORT createBinary(BinaryCreatorArgs args, QString &argumentError); + +} // namespace QInstallerTools + +#endif // BINARYCREATOR_H diff --git a/src/libs/ifwtools/ifwtools.pri b/src/libs/ifwtools/ifwtools.pri new file mode 100644 index 000000000..0ce7b45fc --- /dev/null +++ b/src/libs/ifwtools/ifwtools.pri @@ -0,0 +1,19 @@ +INCLUDEPATH += $$PWD $$PWD/rcc + +DEFINES += BUILD_SHARED_IFWTOOLS + +HEADERS += $$PWD/rcc/rcc.h \ + $$PWD/rcc/qcorecmdlineargs_p.h + +SOURCES += $$PWD/rcc/rcc.cpp \ + $$PWD/rcc/rccmain.cpp + +HEADERS += $$PWD/ifwtools_global.h \ + $$PWD/repositorygen.h \ + $$PWD/binarycreator.h + +SOURCES += $$PWD/repositorygen.cpp \ + $$PWD/binarycreator.cpp + +RESOURCES += $$PWD/resources/ifwtools.qrc + diff --git a/src/libs/ifwtools/ifwtools_global.h b/src/libs/ifwtools/ifwtools_global.h new file mode 100644 index 000000000..ff04efce8 --- /dev/null +++ b/src/libs/ifwtools/ifwtools_global.h @@ -0,0 +1,44 @@ +/************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Installer Framework. +** +** $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$ +** +**************************************************************************/ + +#ifndef IFWTOOLS_GLOBAL_H +#define IFWTOOLS_GLOBAL_H + +#include <QtCore/QtGlobal> + +#ifndef QT_STATIC +# ifdef BUILD_SHARED_IFWTOOLS +# define IFWTOOLS_EXPORT Q_DECL_EXPORT +# else +# define IFWTOOLS_EXPORT Q_DECL_IMPORT +# endif +#else +# define IFWTOOLS_EXPORT +#endif + +#endif //IFWTOOLS_GLOBAL_H diff --git a/src/libs/ifwtools/rcc/qcorecmdlineargs_p.h b/src/libs/ifwtools/rcc/qcorecmdlineargs_p.h new file mode 100644 index 000000000..9c55a43cd --- /dev/null +++ b/src/libs/ifwtools/rcc/qcorecmdlineargs_p.h @@ -0,0 +1,59 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtCore module 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$ +** +****************************************************************************/ + +#ifndef QCORECMDLINEARGS_P_H +#define QCORECMDLINEARGS_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/qstring.h> +#include <QtCore/qstringlist.h> + +QT_BEGIN_NAMESPACE + +static inline QStringList qCmdLineArgs(int argc, char *argv[]) +{ + QStringList args; + for (int i = 0; i != argc; ++i) + args += QString::fromLocal8Bit(argv[i]); + return args; +} + + +QT_END_NAMESPACE + +#endif // QCORECMDLINEARGS_WIN_P_H diff --git a/src/libs/ifwtools/rcc/rcc.cpp b/src/libs/ifwtools/rcc/rcc.cpp new file mode 100644 index 000000000..10b7cbc4f --- /dev/null +++ b/src/libs/ifwtools/rcc/rcc.cpp @@ -0,0 +1,1038 @@ +/************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Installer Framework. +** +** $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 "rcc.h" + +#include <QtCore/QByteArray> +#include <QtCore/QDateTime> +#include <QtCore/QDebug> +#include <QtCore/QDir> +#include <QtCore/QDirIterator> +#include <QtCore/QFile> +#include <QtCore/QIODevice> +#include <QtCore/QLocale> +#include <QtCore/QRegExp> +#include <QtCore/QStack> + +#include <QXmlStreamReader> + +QT_BEGIN_NAMESPACE + +enum { + CONSTANT_USENAMESPACE = 1, + CONSTANT_COMPRESSLEVEL_DEFAULT = -1, + CONSTANT_COMPRESSTHRESHOLD_DEFAULT = 70 +}; + + +#define writeString(s) write(s, sizeof(s)) + +void RCCResourceLibrary::write(const char *str, int len) +{ + --len; // trailing \0 on string literals... + int n = m_out.size(); + m_out.resize(n + len); + memcpy(m_out.data() + n, str, len); +} + +void RCCResourceLibrary::writeByteArray(const QByteArray &other) +{ + m_out.append(other); +} + +static inline QString msgOpenReadFailed(const QString &fname, const QString &why) +{ + return QString::fromUtf8("Unable to open %1 for reading: %2\n").arg(fname).arg(why); +} + + +/////////////////////////////////////////////////////////// +// +// RCCFileInfo +// +/////////////////////////////////////////////////////////// + +class RCCFileInfo +{ +public: + enum Flags + { + NoFlags = 0x00, + Compressed = 0x01, + Directory = 0x02 + }; + + RCCFileInfo(const QString &name = QString(), const QFileInfo &fileInfo = QFileInfo(), + QLocale::Language language = QLocale::C, + QLocale::Country country = QLocale::AnyCountry, + uint flags = NoFlags, + int compressLevel = CONSTANT_COMPRESSLEVEL_DEFAULT, + int compressThreshold = CONSTANT_COMPRESSTHRESHOLD_DEFAULT); + ~RCCFileInfo(); + + QString resourceName() const; + +public: + qint64 writeDataBlob(RCCResourceLibrary &lib, qint64 offset, QString *errorMessage); + qint64 writeDataName(RCCResourceLibrary &, qint64 offset); + void writeDataInfo(RCCResourceLibrary &lib); + + int m_flags; + QString m_name; + QLocale::Language m_language; + QLocale::Country m_country; + QFileInfo m_fileInfo; + RCCFileInfo *m_parent; + QHash<QString, RCCFileInfo*> m_children; + int m_compressLevel; + int m_compressThreshold; + + qint64 m_nameOffset; + qint64 m_dataOffset; + qint64 m_childOffset; +}; + +RCCFileInfo::RCCFileInfo(const QString &name, const QFileInfo &fileInfo, + QLocale::Language language, QLocale::Country country, uint flags, + int compressLevel, int compressThreshold) +{ + m_name = name; + m_fileInfo = fileInfo; + m_language = language; + m_country = country; + m_flags = flags; + m_parent = nullptr; + m_nameOffset = 0; + m_dataOffset = 0; + m_childOffset = 0; + m_compressLevel = compressLevel; + m_compressThreshold = compressThreshold; +} + +RCCFileInfo::~RCCFileInfo() +{ + qDeleteAll(m_children); +} + +QString RCCFileInfo::resourceName() const +{ + QString resource = m_name; + for (RCCFileInfo *p = m_parent; p; p = p->m_parent) + resource = resource.prepend(p->m_name + QLatin1Char('/')); + return QLatin1Char(':') + resource; +} + +void RCCFileInfo::writeDataInfo(RCCResourceLibrary &lib) +{ + const bool text = (lib.m_format == RCCResourceLibrary::C_Code); + //some info + if (text) { + if (m_language != QLocale::C) { + lib.writeString(" // "); + lib.writeByteArray(resourceName().toLocal8Bit()); + lib.writeString(" ["); + lib.writeByteArray(QByteArray::number(m_country)); + lib.writeString("::"); + lib.writeByteArray(QByteArray::number(m_language)); + lib.writeString("[\n "); + } else { + lib.writeString(" // "); + lib.writeByteArray(resourceName().toLocal8Bit()); + lib.writeString("\n "); + } + } + + //pointer data + if (m_flags & RCCFileInfo::Directory) { + // name offset + lib.writeNumber4(m_nameOffset); + + // flags + lib.writeNumber2(m_flags); + + // child count + lib.writeNumber4(m_children.size()); + + // first child offset + lib.writeNumber4(m_childOffset); + } else { + // name offset + lib.writeNumber4(m_nameOffset); + + // flags + lib.writeNumber2(m_flags); + + // locale + lib.writeNumber2(m_country); + lib.writeNumber2(m_language); + + //data offset + lib.writeNumber4(m_dataOffset); + } + if (text) + lib.writeChar('\n'); +} + +qint64 RCCFileInfo::writeDataBlob(RCCResourceLibrary &lib, qint64 offset, + QString *errorMessage) +{ + const bool text = (lib.m_format == RCCResourceLibrary::C_Code); + + //capture the offset + m_dataOffset = offset; + + //find the data to be written + QFile file(m_fileInfo.absoluteFilePath()); + if (!file.open(QFile::ReadOnly)) { + *errorMessage = msgOpenReadFailed(m_fileInfo.absoluteFilePath(), file.errorString()); + return 0; + } + QByteArray data = file.readAll(); + +#ifndef QT_NO_COMPRESS + // Check if compression is useful for this file + if (m_compressLevel != 0 && data.size() != 0) { + QByteArray compressed = + qCompress(reinterpret_cast<uchar *>(data.data()), data.size(), m_compressLevel); + + int compressRatio = int(100.0 * (data.size() - compressed.size()) / data.size()); + if (compressRatio >= m_compressThreshold) { + data = compressed; + m_flags |= Compressed; + } + } +#endif // QT_NO_COMPRESS + + // some info + if (text) { + lib.writeString(" // "); + lib.writeByteArray(m_fileInfo.absoluteFilePath().toLocal8Bit()); + lib.writeString("\n "); + } + + // write the length + + lib.writeNumber4(data.size()); + if (text) + lib.writeString("\n "); + offset += 4; + + // write the payload + const char *p = data.constData(); + if (text) { + for (int i = data.size(), j = 0; --i >= 0; --j) { + lib.writeHex(*p++); + if (j == 0) { + lib.writeString("\n "); + j = 16; + } + } + } else { + for (int i = data.size(); --i >= 0; ) + lib.writeChar(*p++); + } + offset += data.size(); + + // done + if (text) + lib.writeString("\n "); + return offset; +} + +qint64 RCCFileInfo::writeDataName(RCCResourceLibrary &lib, qint64 offset) +{ + const bool text = (lib.m_format == RCCResourceLibrary::C_Code); + + // capture the offset + m_nameOffset = offset; + + // some info + if (text) { + lib.writeString(" // "); + lib.writeByteArray(m_name.toLocal8Bit()); + lib.writeString("\n "); + } + + // write the length + lib.writeNumber2(m_name.length()); + if (text) + lib.writeString("\n "); + offset += 2; + + // write the hash + lib.writeNumber4(qt_hash(m_name)); + if (text) + lib.writeString("\n "); + offset += 4; + + // write the m_name + const QChar *unicode = m_name.unicode(); + for (int i = 0; i < m_name.length(); ++i) { + lib.writeNumber2(unicode[i].unicode()); + if (text && i % 16 == 0) + lib.writeString("\n "); + } + offset += m_name.length()*2; + + // done + if (text) + lib.writeString("\n "); + return offset; +} + + +/////////////////////////////////////////////////////////// +// +// RCCResourceLibrary +// +/////////////////////////////////////////////////////////// + +RCCResourceLibrary::Strings::Strings() : + TAG_RCC(QLatin1String("RCC")), + TAG_RESOURCE(QLatin1String("qresource")), + TAG_FILE(QLatin1String("file")), + ATTRIBUTE_LANG(QLatin1String("lang")), + ATTRIBUTE_PREFIX(QLatin1String("prefix")), + ATTRIBUTE_ALIAS(QLatin1String("alias")), + ATTRIBUTE_THRESHOLD(QLatin1String("threshold")), + ATTRIBUTE_COMPRESS(QLatin1String("compress")) +{ +} + +RCCResourceLibrary::RCCResourceLibrary() + : m_root(nullptr), + m_format(C_Code), + m_verbose(false), + m_compressLevel(CONSTANT_COMPRESSLEVEL_DEFAULT), + m_compressThreshold(CONSTANT_COMPRESSTHRESHOLD_DEFAULT), + m_treeOffset(0), + m_namesOffset(0), + m_dataOffset(0), + m_useNameSpace(CONSTANT_USENAMESPACE), + m_errorDevice(nullptr) +{ + m_out.reserve(30 * 1000 * 1000); +} + +RCCResourceLibrary::~RCCResourceLibrary() +{ + delete m_root; +} + +enum RCCXmlTag { + RccTag, + ResourceTag, + FileTag +}; + +bool RCCResourceLibrary::interpretResourceFile(QIODevice *inputDevice, + const QString &fname, QString currentPath, bool ignoreErrors) +{ + Q_ASSERT(m_errorDevice); + const QChar slash = QLatin1Char('/'); + if (!currentPath.isEmpty() && !currentPath.endsWith(slash)) + currentPath += slash; + + QXmlStreamReader reader(inputDevice); + QStack<RCCXmlTag> tokens; + + QString prefix; + QLocale::Language language = QLocale::c().language(); + QLocale::Country country = QLocale::c().country(); + QString alias; + int compressLevel = m_compressLevel; + int compressThreshold = m_compressThreshold; + + while (!reader.atEnd()) { + QXmlStreamReader::TokenType t = reader.readNext(); + switch (t) { + case QXmlStreamReader::StartElement: + if (reader.name() == m_strings.TAG_RCC) { + if (!tokens.isEmpty()) + reader.raiseError(QLatin1String("expected <RCC> tag")); + else + tokens.push(RccTag); + } else if (reader.name() == m_strings.TAG_RESOURCE) { + if (tokens.isEmpty() || tokens.top() != RccTag) { + reader.raiseError(QLatin1String("unexpected <RESOURCE> tag")); + } else { + tokens.push(ResourceTag); + + QXmlStreamAttributes attributes = reader.attributes(); + language = QLocale::c().language(); + country = QLocale::c().country(); + + if (attributes.hasAttribute(m_strings.ATTRIBUTE_LANG)) { + QString attribute = attributes.value(m_strings.ATTRIBUTE_LANG).toString(); + QLocale lang = QLocale(attribute); + language = lang.language(); + if (2 == attribute.length()) { + // Language only + country = QLocale::AnyCountry; + } else { + country = lang.country(); + } + } + + prefix.clear(); + if (attributes.hasAttribute(m_strings.ATTRIBUTE_PREFIX)) + prefix = attributes.value(m_strings.ATTRIBUTE_PREFIX).toString(); + if (!prefix.startsWith(slash)) + prefix.prepend(slash); + if (!prefix.endsWith(slash)) + prefix += slash; + } + } else if (reader.name() == m_strings.TAG_FILE) { + if (tokens.isEmpty() || tokens.top() != ResourceTag) { + reader.raiseError(QLatin1String("unexpected <FILE> tag")); + } else { + tokens.push(FileTag); + + QXmlStreamAttributes attributes = reader.attributes(); + alias.clear(); + if (attributes.hasAttribute(m_strings.ATTRIBUTE_ALIAS)) + alias = attributes.value(m_strings.ATTRIBUTE_ALIAS).toString(); + + compressLevel = m_compressLevel; + if (attributes.hasAttribute(m_strings.ATTRIBUTE_COMPRESS)) + compressLevel = attributes.value(m_strings.ATTRIBUTE_COMPRESS).toString().toInt(); + + compressThreshold = m_compressThreshold; + if (attributes.hasAttribute(m_strings.ATTRIBUTE_THRESHOLD)) + compressThreshold = attributes.value(m_strings.ATTRIBUTE_THRESHOLD).toString().toInt(); + + // Special case for -no-compress. Overrides all other settings. + if (m_compressLevel == -2) + compressLevel = 0; + } + } else { + reader.raiseError(QString(QLatin1String("unexpected tag: %1")).arg(reader.name().toString())); + } + break; + + case QXmlStreamReader::EndElement: + if (reader.name() == m_strings.TAG_RCC) { + if (!tokens.isEmpty() && tokens.top() == RccTag) + tokens.pop(); + else + reader.raiseError(QLatin1String("unexpected closing tag")); + } else if (reader.name() == m_strings.TAG_RESOURCE) { + if (!tokens.isEmpty() && tokens.top() == ResourceTag) + tokens.pop(); + else + reader.raiseError(QLatin1String("unexpected closing tag")); + } else if (reader.name() == m_strings.TAG_FILE) { + if (!tokens.isEmpty() && tokens.top() == FileTag) + tokens.pop(); + else + reader.raiseError(QLatin1String("unexpected closing tag")); + } + break; + + case QXmlStreamReader::Characters: + if (reader.isWhitespace()) + break; + if (tokens.isEmpty() || tokens.top() != FileTag) { + reader.raiseError(QLatin1String("unexpected text")); + } else { + QString fileName = reader.text().toString(); + if (fileName.isEmpty()) { + const QString msg = QString::fromLatin1("RCC: Warning: Null node in XML of '%1'\n").arg(fname); + m_errorDevice->write(msg.toUtf8()); + } + + if (alias.isNull()) + alias = fileName; + + alias = QDir::cleanPath(alias); + while (alias.startsWith(QLatin1String("../"))) + alias.remove(0, 3); + alias = QDir::cleanPath(m_resourceRoot) + prefix + alias; + + QString absFileName = fileName; + if (QDir::isRelativePath(absFileName)) + absFileName.prepend(currentPath); + QFileInfo file(absFileName); + if (!file.exists()) { + m_failedResources.push_back(absFileName); + const QString msg = QString::fromLatin1("RCC: Error in '%1': Cannot find file '%2'\n").arg(fname).arg(fileName); + m_errorDevice->write(msg.toUtf8()); + if (ignoreErrors) + continue; + else + return false; + } else if (file.isFile()) { + const bool arc = + addFile(alias, + RCCFileInfo(alias.section(slash, -1), + file, + language, + country, + RCCFileInfo::NoFlags, + compressLevel, + compressThreshold) + ); + if (!arc) + m_failedResources.push_back(absFileName); + } else { + QDir dir; + if (file.isDir()) { + dir.setPath(file.filePath()); + } else { + dir.setPath(file.path()); + dir.setNameFilters(QStringList(file.fileName())); + if (alias.endsWith(file.fileName())) + alias = alias.left(alias.length()-file.fileName().length()); + } + if (!alias.endsWith(slash)) + alias += slash; + QDirIterator it(dir, QDirIterator::FollowSymlinks|QDirIterator::Subdirectories); + while (it.hasNext()) { + it.next(); + QFileInfo child(it.fileInfo()); + if (child.fileName() != QLatin1String(".") && child.fileName() != QLatin1String("..")) { + const bool arc = + addFile(alias + child.fileName(), + RCCFileInfo(child.fileName(), + child, + language, + country, + child.isDir() ? RCCFileInfo::Directory : RCCFileInfo::NoFlags, + compressLevel, + compressThreshold) + ); + if (!arc) + m_failedResources.push_back(child.fileName()); + } + } + } + } + break; + + default: + break; + } + } + + if (reader.hasError()) { + if (ignoreErrors) + return true; + int errorLine = reader.lineNumber(); + int errorColumn = reader.columnNumber(); + QString errorMessage = reader.errorString(); + QString msg = QString::fromLatin1("RCC Parse Error: '%1' Line: %2 Column: %3 [%4]\n").arg(fname).arg(errorLine).arg(errorColumn).arg(errorMessage); + m_errorDevice->write(msg.toUtf8()); + return false; + } + + if (m_root == nullptr) { + const QString msg = QString::fromUtf8("RCC: Warning: No resources in '%1'.\n").arg(fname); + m_errorDevice->write(msg.toUtf8()); + if (!ignoreErrors && m_format == Binary) { + // create dummy entry, otherwise loading with QResource will crash + m_root = new RCCFileInfo(QString(), QFileInfo(), + QLocale::C, QLocale::AnyCountry, RCCFileInfo::Directory); + } + } + + return true; +} + +bool RCCResourceLibrary::addFile(const QString &alias, const RCCFileInfo &file) +{ + Q_ASSERT(m_errorDevice); + if (file.m_fileInfo.size() > 0xffffffff) { + const QString msg = QString::fromUtf8("File too big: %1\n").arg(file.m_fileInfo.absoluteFilePath()); + m_errorDevice->write(msg.toUtf8()); + return false; + } + if (!m_root) + m_root = new RCCFileInfo(QString(), QFileInfo(), QLocale::C, QLocale::AnyCountry, RCCFileInfo::Directory); + + RCCFileInfo *parent = m_root; + const QStringList nodes = alias.split(QLatin1Char('/')); + for (int i = 1; i < nodes.size()-1; ++i) { + const QString node = nodes.at(i); + if (node.isEmpty()) + continue; + if (!parent->m_children.contains(node)) { + RCCFileInfo *s = new RCCFileInfo(node, QFileInfo(), QLocale::C, QLocale::AnyCountry, RCCFileInfo::Directory); + s->m_parent = parent; + parent->m_children.insert(node, s); + parent = s; + } else { + parent = parent->m_children[node]; + } + } + + const QString filename = nodes.at(nodes.size()-1); + RCCFileInfo *s = new RCCFileInfo(file); + s->m_parent = parent; + if (parent->m_children.contains(filename)) { + foreach (const QString &fileName, m_fileNames) + qWarning("%s: Warning: potential duplicate alias detected: '%s'", + qPrintable(fileName), qPrintable(filename)); + } + parent->m_children.insertMulti(filename, s); + return true; +} + +void RCCResourceLibrary::reset() +{ + if (m_root) { + delete m_root; + m_root = nullptr; + } + m_errorDevice = nullptr; + m_failedResources.clear(); +} + + +bool RCCResourceLibrary::readFiles(bool ignoreErrors, QIODevice &errorDevice) +{ + reset(); + m_errorDevice = &errorDevice; + //read in data + if (m_verbose) { + const QString msg = QString::fromUtf8("Processing %1 files [%2]\n") + .arg(m_fileNames.size()).arg(static_cast<int>(ignoreErrors)); + m_errorDevice->write(msg.toUtf8()); + } + for (int i = 0; i < m_fileNames.size(); ++i) { + QFile fileIn; + QString fname = m_fileNames.at(i); + QString pwd; + if (fname == QLatin1String("-")) { + fname = QLatin1String("(stdin)"); + pwd = QDir::currentPath(); + fileIn.setFileName(fname); + if (!fileIn.open(stdin, QIODevice::ReadOnly)) { + m_errorDevice->write(msgOpenReadFailed(fname, fileIn.errorString()).toUtf8()); + return false; + } + } else { + pwd = QFileInfo(fname).path(); + fileIn.setFileName(fname); + if (!fileIn.open(QIODevice::ReadOnly)) { + m_errorDevice->write(msgOpenReadFailed(fname, fileIn.errorString()).toUtf8()); + return false; + } + } + if (m_verbose) { + const QString msg = QString::fromUtf8("Interpreting %1\n").arg(fname); + m_errorDevice->write(msg.toUtf8()); + } + + if (!interpretResourceFile(&fileIn, fname, pwd, ignoreErrors)) + return false; + } + return true; +} + +QStringList RCCResourceLibrary::dataFiles() const +{ + QStringList ret; + QStack<RCCFileInfo*> pending; + + if (!m_root) + return ret; + pending.push(m_root); + while (!pending.isEmpty()) { + RCCFileInfo *file = pending.pop(); + for (QHash<QString, RCCFileInfo*>::iterator it = file->m_children.begin(); + it != file->m_children.end(); ++it) { + RCCFileInfo *child = it.value(); + if (child->m_flags & RCCFileInfo::Directory) + pending.push(child); + ret.append(child->m_fileInfo.filePath()); + } + } + return ret; +} + +// Determine map of resource identifier (':/newPrefix/images/p1.png') to file via recursion +static void resourceDataFileMapRecursion(const RCCFileInfo *m_root, const QString &path, RCCResourceLibrary::ResourceDataFileMap &m) +{ + typedef QHash<QString, RCCFileInfo*>::const_iterator ChildConstIterator; + const QChar slash = QLatin1Char('/'); + const ChildConstIterator cend = m_root->m_children.constEnd(); + for (ChildConstIterator it = m_root->m_children.constBegin(); it != cend; ++it) { + const RCCFileInfo *child = it.value(); + QString childName = path; + childName += slash; + childName += child->m_name; + if (child->m_flags & RCCFileInfo::Directory) { + resourceDataFileMapRecursion(child, childName, m); + } else { + m.insert(childName, child->m_fileInfo.filePath()); + } + } +} + +RCCResourceLibrary::ResourceDataFileMap RCCResourceLibrary::resourceDataFileMap() const +{ + ResourceDataFileMap rc; + if (m_root) + resourceDataFileMapRecursion(m_root, QString(QLatin1Char(':')), rc); + return rc; +} + +bool RCCResourceLibrary::output(QIODevice &outDevice, QIODevice &errorDevice) +{ + m_errorDevice = &errorDevice; + //write out + if (m_verbose) + m_errorDevice->write("Outputting code\n"); + if (!writeHeader()) { + m_errorDevice->write("Cannot write header\n"); + return false; + } + if (m_root) { + if (!writeDataBlobs()) { + m_errorDevice->write("Cannot write data blobs.\n"); + return false; + } + if (!writeDataNames()) { + m_errorDevice->write("Cannot write file names\n"); + return false; + } + if (!writeDataStructure()) { + m_errorDevice->write("Cannot write data tree\n"); + return false; + } + } + if (!writeInitializer()) { + m_errorDevice->write("Cannot write footer\n"); + return false; + } + outDevice.write(m_out.constData(), m_out.size()); + return true; +} + +void RCCResourceLibrary::writeHex(quint8 tmp) +{ + const char digits[] = "0123456789abcdef"; + writeChar('0'); + writeChar('x'); + if (tmp < 16) { + writeChar(digits[tmp]); + } else { + writeChar(digits[tmp >> 4]); + writeChar(digits[tmp & 0xf]); + } + writeChar(','); +} + +void RCCResourceLibrary::writeNumber2(quint16 number) +{ + if (m_format == RCCResourceLibrary::Binary) { + writeChar(number >> 8); + writeChar(number); + } else { + writeHex(number >> 8); + writeHex(number); + } +} + +void RCCResourceLibrary::writeNumber4(quint32 number) +{ + if (m_format == RCCResourceLibrary::Binary) { + writeChar(number >> 24); + writeChar(number >> 16); + writeChar(number >> 8); + writeChar(number); + } else { + writeHex(number >> 24); + writeHex(number >> 16); + writeHex(number >> 8); + writeHex(number); + } +} + +bool RCCResourceLibrary::writeHeader() +{ + if (m_format == C_Code) { + writeString("/****************************************************************************\n"); + writeString("** Resource object code\n"); + writeString("**\n"); + writeString("** Created: "); + writeByteArray(QDateTime::currentDateTime().toString().toLatin1()); + writeString("\n** by: The Resource Compiler for Qt version "); + writeByteArray(QT_VERSION_STR); + writeString("\n**\n"); + writeString("** WARNING! All changes made in this file will be lost!\n"); + writeString( "*****************************************************************************/\n\n"); + writeString("#include <QtCore/qglobal.h>\n\n"); + } else if (m_format == Binary) { + writeString("qres"); + writeNumber4(0); + writeNumber4(0); + writeNumber4(0); + writeNumber4(0); + } + return true; +} + +bool RCCResourceLibrary::writeDataBlobs() +{ + Q_ASSERT(m_errorDevice); + if (m_format == C_Code) + writeString("static const unsigned char qt_resource_data[] = {\n"); + else if (m_format == Binary) + m_dataOffset = m_out.size(); + QStack<RCCFileInfo*> pending; + + if (!m_root) + return false; + + pending.push(m_root); + qint64 offset = 0; + QString errorMessage; + while (!pending.isEmpty()) { + RCCFileInfo *file = pending.pop(); + for (QHash<QString, RCCFileInfo*>::iterator it = file->m_children.begin(); + it != file->m_children.end(); ++it) { + RCCFileInfo *child = it.value(); + if (child->m_flags & RCCFileInfo::Directory) + pending.push(child); + else { + offset = child->writeDataBlob(*this, offset, &errorMessage); + if (offset == 0) { + m_errorDevice->write(errorMessage.toUtf8()); + return false; + } + } + } + } + if (m_format == C_Code) + writeString("\n};\n\n"); + return true; +} + +bool RCCResourceLibrary::writeDataNames() +{ + if (m_format == C_Code) + writeString("static const unsigned char qt_resource_name[] = {\n"); + else if (m_format == Binary) + m_namesOffset = m_out.size(); + + QHash<QString, int> names; + QStack<RCCFileInfo*> pending; + + if (!m_root) + return false; + + pending.push(m_root); + qint64 offset = 0; + while (!pending.isEmpty()) { + RCCFileInfo *file = pending.pop(); + for (QHash<QString, RCCFileInfo*>::iterator it = file->m_children.begin(); + it != file->m_children.end(); ++it) { + RCCFileInfo *child = it.value(); + if (child->m_flags & RCCFileInfo::Directory) + pending.push(child); + if (names.contains(child->m_name)) { + child->m_nameOffset = names.value(child->m_name); + } else { + names.insert(child->m_name, offset); + offset = child->writeDataName(*this, offset); + } + } + } + if (m_format == C_Code) + writeString("\n};\n\n"); + return true; +} + +static bool qt_rcc_compare_hash(const RCCFileInfo *left, const RCCFileInfo *right) +{ + return qt_hash(left->m_name) < qt_hash(right->m_name); +} + +bool RCCResourceLibrary::writeDataStructure() +{ + if (m_format == C_Code) + writeString("static const unsigned char qt_resource_struct[] = {\n"); + else if (m_format == Binary) + m_treeOffset = m_out.size(); + QStack<RCCFileInfo*> pending; + + if (!m_root) + return false; + + //calculate the child offsets (flat) + pending.push(m_root); + int offset = 1; + while (!pending.isEmpty()) { + RCCFileInfo *file = pending.pop(); + file->m_childOffset = offset; + + //sort by hash value for binary lookup + QList<RCCFileInfo*> m_children = file->m_children.values(); + qSort(m_children.begin(), m_children.end(), qt_rcc_compare_hash); + + //write out the actual data now + for (int i = 0; i < m_children.size(); ++i) { + RCCFileInfo *child = m_children.at(i); + ++offset; + if (child->m_flags & RCCFileInfo::Directory) + pending.push(child); + } + } + + //write out the structure (ie iterate again!) + pending.push(m_root); + m_root->writeDataInfo(*this); + while (!pending.isEmpty()) { + RCCFileInfo *file = pending.pop(); + + //sort by hash value for binary lookup + QList<RCCFileInfo*> m_children = file->m_children.values(); + qSort(m_children.begin(), m_children.end(), qt_rcc_compare_hash); + + //write out the actual data now + for (int i = 0; i < m_children.size(); ++i) { + RCCFileInfo *child = m_children.at(i); + child->writeDataInfo(*this); + if (child->m_flags & RCCFileInfo::Directory) + pending.push(child); + } + } + if (m_format == C_Code) + writeString("\n};\n\n"); + + return true; +} + +void RCCResourceLibrary::writeMangleNamespaceFunction(const QByteArray &name) +{ + if (m_useNameSpace) { + writeString("QT_MANGLE_NAMESPACE("); + writeByteArray(name); + writeChar(')'); + } else { + writeByteArray(name); + } +} + +void RCCResourceLibrary::writeAddNamespaceFunction(const QByteArray &name) +{ + if (m_useNameSpace) { + writeString("QT_PREPEND_NAMESPACE("); + writeByteArray(name); + writeChar(')'); + } else { + writeByteArray(name); + } +} + +bool RCCResourceLibrary::writeInitializer() +{ + if (m_format == C_Code) { + //write("\nQT_BEGIN_NAMESPACE\n"); + QString initName = m_initName; + if (!initName.isEmpty()) { + initName.prepend(QLatin1Char('_')); + initName.replace(QRegExp(QLatin1String("[^a-zA-Z0-9_]")), QLatin1String("_")); + } + + //init + if (m_useNameSpace) + writeString("QT_BEGIN_NAMESPACE\n\n"); + if (m_root) { + writeString("extern Q_CORE_EXPORT bool qRegisterResourceData\n " + "(int, const unsigned char *, " + "const unsigned char *, const unsigned char *);\n\n"); + writeString("extern Q_CORE_EXPORT bool qUnregisterResourceData\n " + "(int, const unsigned char *, " + "const unsigned char *, const unsigned char *);\n\n"); + } + if (m_useNameSpace) + writeString("QT_END_NAMESPACE\n\n\n"); + QString initResources = QLatin1String("qInitResources"); + initResources += initName; + writeString("int "); + writeMangleNamespaceFunction(initResources.toLatin1()); + writeString("()\n{\n"); + + if (m_root) { + writeString(" "); + writeAddNamespaceFunction("qRegisterResourceData"); + writeString("\n (0x01, qt_resource_struct, " + "qt_resource_name, qt_resource_data);\n"); + } + writeString(" return 1;\n"); + writeString("}\n\n"); + writeString("Q_CONSTRUCTOR_FUNCTION("); + writeMangleNamespaceFunction(initResources.toLatin1()); + writeString(")\n\n"); + + //cleanup + QString cleanResources = QLatin1String("qCleanupResources"); + cleanResources += initName; + writeString("int "); + writeMangleNamespaceFunction(cleanResources.toLatin1()); + writeString("()\n{\n"); + if (m_root) { + writeString(" "); + writeAddNamespaceFunction("qUnregisterResourceData"); + writeString("\n (0x01, qt_resource_struct, " + "qt_resource_name, qt_resource_data);\n"); + } + writeString(" return 1;\n"); + writeString("}\n\n"); + writeString("Q_DESTRUCTOR_FUNCTION("); + writeMangleNamespaceFunction(cleanResources.toLatin1()); + writeString(")\n\n"); + } else if (m_format == Binary) { + int i = 4; + char *p = m_out.data(); + p[i++] = 0; // 0x01 + p[i++] = 0; + p[i++] = 0; + p[i++] = 1; + + p[i++] = (m_treeOffset >> 24) & 0xff; + p[i++] = (m_treeOffset >> 16) & 0xff; + p[i++] = (m_treeOffset >> 8) & 0xff; + p[i++] = (m_treeOffset >> 0) & 0xff; + + p[i++] = (m_dataOffset >> 24) & 0xff; + p[i++] = (m_dataOffset >> 16) & 0xff; + p[i++] = (m_dataOffset >> 8) & 0xff; + p[i++] = (m_dataOffset >> 0) & 0xff; + + p[i++] = (m_namesOffset >> 24) & 0xff; + p[i++] = (m_namesOffset >> 16) & 0xff; + p[i++] = (m_namesOffset >> 8) & 0xff; + p[i++] = (m_namesOffset >> 0) & 0xff; + } + return true; +} + +QT_END_NAMESPACE diff --git a/src/libs/ifwtools/rcc/rcc.h b/src/libs/ifwtools/rcc/rcc.h new file mode 100644 index 000000000..ecf408dbc --- /dev/null +++ b/src/libs/ifwtools/rcc/rcc.h @@ -0,0 +1,140 @@ +/************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Installer Framework. +** +** $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$ +** +**************************************************************************/ + +#ifndef RCC_H +#define RCC_H + +#include <QtCore/QStringList> +#include <QtCore/QHash> +#include <QtCore/QString> + +QT_BEGIN_NAMESPACE + +class RCCFileInfo; +class QIODevice; +class QTextStream; + + +class RCCResourceLibrary +{ + RCCResourceLibrary(const RCCResourceLibrary &); + RCCResourceLibrary &operator=(const RCCResourceLibrary &); + +public: + RCCResourceLibrary(); + ~RCCResourceLibrary(); + + bool output(QIODevice &out, QIODevice &errorDevice); + + bool readFiles(bool ignoreErrors, QIODevice &errorDevice); + + enum Format { Binary, C_Code }; + void setFormat(Format f) { m_format = f; } + Format format() const { return m_format; } + + void setInputFiles(const QStringList &files) { m_fileNames = files; } + QStringList inputFiles() const { return m_fileNames; } + + QStringList dataFiles() const; + + // Return a map of resource identifier (':/newPrefix/images/p1.png') to file. + typedef QHash<QString, QString> ResourceDataFileMap; + ResourceDataFileMap resourceDataFileMap() const; + + void setVerbose(bool b) { m_verbose = b; } + bool verbose() const { return m_verbose; } + + void setInitName(const QString &name) { m_initName = name; } + QString initName() const { return m_initName; } + + void setCompressLevel(int c) { m_compressLevel = c; } + int compressLevel() const { return m_compressLevel; } + + void setCompressThreshold(int t) { m_compressThreshold = t; } + int compressThreshold() const { return m_compressThreshold; } + + void setResourceRoot(const QString &root) { m_resourceRoot = root; } + QString resourceRoot() const { return m_resourceRoot; } + + void setUseNameSpace(bool v) { m_useNameSpace = v; } + bool useNameSpace() const { return m_useNameSpace; } + + QStringList failedResources() const { return m_failedResources; } + +private: + struct Strings { + Strings(); + const QString TAG_RCC; + const QString TAG_RESOURCE; + const QString TAG_FILE; + const QString ATTRIBUTE_LANG; + const QString ATTRIBUTE_PREFIX; + const QString ATTRIBUTE_ALIAS; + const QString ATTRIBUTE_THRESHOLD; + const QString ATTRIBUTE_COMPRESS; + }; + friend class RCCFileInfo; + void reset(); + bool addFile(const QString &alias, const RCCFileInfo &file); + bool interpretResourceFile(QIODevice *inputDevice, const QString &file, + QString currentPath = QString(), bool ignoreErrors = false); + bool writeHeader(); + bool writeDataBlobs(); + bool writeDataNames(); + bool writeDataStructure(); + bool writeInitializer(); + void writeMangleNamespaceFunction(const QByteArray &name); + void writeAddNamespaceFunction(const QByteArray &name); + void writeHex(quint8 number); + void writeNumber2(quint16 number); + void writeNumber4(quint32 number); + void writeChar(char c) { m_out.append(c); } + void writeByteArray(const QByteArray &); + void write(const char *, int len); + + const Strings m_strings; + RCCFileInfo *m_root; + QStringList m_fileNames; + QString m_resourceRoot; + QString m_initName; + Format m_format; + bool m_verbose; + int m_compressLevel; + int m_compressThreshold; + int m_treeOffset; + int m_namesOffset; + int m_dataOffset; + bool m_useNameSpace; + QStringList m_failedResources; + QIODevice *m_errorDevice; + QByteArray m_out; +}; + +QT_END_NAMESPACE + +#endif // RCC_H diff --git a/src/libs/ifwtools/rcc/rccmain.cpp b/src/libs/ifwtools/rcc/rccmain.cpp new file mode 100644 index 000000000..47d8fa8de --- /dev/null +++ b/src/libs/ifwtools/rcc/rccmain.cpp @@ -0,0 +1,239 @@ +/************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Installer Framework. +** +** $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 "rcc.h" +#include "qcorecmdlineargs_p.h" + +#include <QDebug> +#include <QDir> +#include <QFile> +#include <QFileInfo> +#include <QTextStream> + +QT_BEGIN_NAMESPACE + +void showHelp(const QString &argv0, const QString &error) +{ + fprintf(stderr, "Qt resource compiler\n"); + if (!error.isEmpty()) + fprintf(stderr, "%s: %s\n", qPrintable(argv0), qPrintable(error)); + fprintf(stderr, "Usage: %s [options] <inputs>\n\n" + "Options:\n" + " -o file write output to file rather than stdout\n" + " -name name create an external initialization function with name\n" + " -threshold level threshold to consider compressing files\n" + " -compress level compress input files by level\n" + " -root path prefix resource access path with root path\n" + " -no-compress disable all compression\n" + " -binary output a binary file for use as a dynamic resource\n" + " -namespace turn off namespace macros\n" + " -project Output a resource file containing all\n" + " files from the current directory\n" + " -version display version\n" + " -help display this information\n", + qPrintable(argv0)); +} + +void dumpRecursive(const QDir &dir, QTextStream &out) +{ + QFileInfoList entries = dir.entryInfoList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot + | QDir::NoSymLinks); + foreach (const QFileInfo &entry, entries) { + if (entry.isDir()) { + dumpRecursive(entry.filePath(), out); + } else { + out << QLatin1String("<file>") + << entry.filePath() + << QLatin1String("</file>\n"); + } + } +} + +int createProject(const QString &outFileName) +{ + QFile file; + bool isOk = false; + if (outFileName.isEmpty()) { + isOk = file.open(stdout, QFile::WriteOnly | QFile::Text); + } else { + file.setFileName(outFileName); + isOk = file.open(QFile::WriteOnly | QFile::Text); + } + if (!isOk) { + fprintf(stderr, "Unable to open %s: %s\n", + outFileName.isEmpty() ? qPrintable(outFileName) : "standard output", + qPrintable(file.errorString())); + return 1; + } + + QTextStream out(&file); + out << QLatin1String("<!DOCTYPE RCC><RCC version=\"1.0\">\n" + "<qresource>\n"); + + // use "." as dir to get relative file paths + dumpRecursive(QDir(QLatin1String(".")), out); + + out << QLatin1String("</qresource>\n" + "</RCC>\n"); + + return 0; +} + +int runRcc(int argc, char *argv[]) +{ + QString outFilename; + bool helpRequested = false; + bool list = false; + bool projectRequested = false; + QStringList filenamesIn; + + QStringList args = qCmdLineArgs(argc, argv); + + RCCResourceLibrary library; + + //parse options + QString errorMsg; + for (int i = 1; i < args.count() && errorMsg.isEmpty(); i++) { + if (args[i].isEmpty()) + continue; + if (args[i][0] == QLatin1Char('-')) { // option + QString opt = args[i]; + if (opt == QLatin1String("-o")) { + if (!(i < argc-1)) { + errorMsg = QLatin1String("Missing output name"); + break; + } + outFilename = args[++i]; + } else if (opt == QLatin1String("-name")) { + if (!(i < argc-1)) { + errorMsg = QLatin1String("Missing target name"); + break; + } + library.setInitName(args[++i]); + } else if (opt == QLatin1String("-root")) { + if (!(i < argc-1)) { + errorMsg = QLatin1String("Missing root path"); + break; + } + library.setResourceRoot(QDir::cleanPath(args[++i])); + if (library.resourceRoot().isEmpty() + || library.resourceRoot().at(0) != QLatin1Char('/')) + errorMsg = QLatin1String("Root must start with a /"); + } else if (opt == QLatin1String("-compress")) { + if (!(i < argc-1)) { + errorMsg = QLatin1String("Missing compression level"); + break; + } + library.setCompressLevel(args[++i].toInt()); + } else if (opt == QLatin1String("-threshold")) { + if (!(i < argc-1)) { + errorMsg = QLatin1String("Missing compression threshold"); + break; + } + library.setCompressThreshold(args[++i].toInt()); + } else if (opt == QLatin1String("-binary")) { + library.setFormat(RCCResourceLibrary::Binary); + } else if (opt == QLatin1String("-namespace")) { + library.setUseNameSpace(!library.useNameSpace()); + } else if (opt == QLatin1String("-verbose")) { + library.setVerbose(true); + } else if (opt == QLatin1String("-list")) { + list = true; + } else if (opt == QLatin1String("-version") || opt == QLatin1String("-v")) { + fprintf(stderr, "Qt Resource Compiler version %s\n", QT_VERSION_STR); + return 1; + } else if (opt == QLatin1String("-help") || opt == QLatin1String("-h")) { + helpRequested = true; + } else if (opt == QLatin1String("-no-compress")) { + library.setCompressLevel(-2); + } else if (opt == QLatin1String("-project")) { + projectRequested = true; + } else { + errorMsg = QString::fromLatin1("Unknown option: '%1'").arg(args[i]); + } + } else { + if (!QFile::exists(args[i])) { + qWarning("%s: File does not exist '%s'", + qPrintable(args[0]), qPrintable(args[i])); + return 1; + } + filenamesIn.append(args[i]); + } + } + + if (projectRequested && !helpRequested) { + return createProject(outFilename); + } + + if (!filenamesIn.size() || !errorMsg.isEmpty() || helpRequested) { + showHelp(args[0], errorMsg); + return 1; + } + QFile errorDevice; + errorDevice.open(stderr, QIODevice::WriteOnly|QIODevice::Text); + + if (library.verbose()) + errorDevice.write("Qt resource compiler\n"); + + library.setInputFiles(filenamesIn); + + if (!library.readFiles(list, errorDevice)) + return 1; + + // open output + QFile out; + QIODevice::OpenMode mode = QIODevice::WriteOnly; + if (library.format() == RCCResourceLibrary::C_Code) + mode |= QIODevice::Text; + + if (outFilename.isEmpty() || outFilename == QLatin1String("-")) { + // using this overload close() only flushes. + out.open(stdout, mode); + } else { + out.setFileName(outFilename); + if (!out.open(mode)) { + const QString msg = QString::fromUtf8("Unable to open %1 for writing: %2\n").arg(outFilename).arg(out.errorString()); + errorDevice.write(msg.toUtf8()); + return 1; + } + } + + // do the task + if (list) { + const QStringList data = library.dataFiles(); + for (int i = 0; i < data.size(); ++i) { + out.write(qPrintable(QDir::cleanPath(data.at(i)))); + out.write("\n"); + } + return 0; + } + + return library.output(out, errorDevice) ? 0 : 1; +} + +QT_END_NAMESPACE diff --git a/src/libs/ifwtools/repositorygen.cpp b/src/libs/ifwtools/repositorygen.cpp new file mode 100644 index 000000000..819ab1ac8 --- /dev/null +++ b/src/libs/ifwtools/repositorygen.cpp @@ -0,0 +1,966 @@ +/************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Installer Framework. +** +** $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 "repositorygen.h" + +#include "constants.h" +#include "fileio.h" +#include "fileutils.h" +#include "errors.h" +#include "globals.h" +#include "lib7z_create.h" +#include "lib7z_extract.h" +#include "lib7z_facade.h" +#include "lib7z_list.h" +#include "settings.h" +#include "qinstallerglobal.h" +#include "utils.h" +#include "scriptengine.h" + +#include "updater.h" + +#include <QtCore/QDirIterator> +#include <QtCore/QRegExp> + +#include <QtXml/QDomDocument> +#include <QTemporaryDir> + +#include <iostream> + +using namespace QInstaller; +using namespace QInstallerTools; + +void QInstallerTools::printRepositoryGenOptions() +{ + std::cout << " -p|--packages dir The directory containing the available packages." << std::endl; + std::cout << " This entry can be given multiple times." << std::endl; + std::cout << " --repository dir The directory containing the available repository." << std::endl; + std::cout << " This entry can be given multiple times." << std::endl; + + std::cout << " -e|--exclude p1,...,pn Exclude the given packages." << std::endl; + std::cout << " -i|--include p1,...,pn Include the given packages and their dependencies" << std::endl; + std::cout << " from the repository." << std::endl; + + std::cout << " --ignore-translations Do not use any translation" << std::endl; + std::cout << " --ignore-invalid-packages Ignore all invalid packages instead of aborting." << std::endl; + std::cout << " --ignore-invalid-repositories Ignore all invalid repositories instead of aborting." << std::endl; +} + +QString QInstallerTools::makePathAbsolute(const QString &path) +{ + if (QFileInfo(path).isRelative()) + return QDir::current().absoluteFilePath(path); + return path; +} + +void QInstallerTools::copyWithException(const QString &source, const QString &target, const QString &kind) +{ + qDebug() << "Copying associated" << kind << "file" << source; + + const QFileInfo targetFileInfo(target); + if (!targetFileInfo.dir().exists()) + QInstaller::mkpath(targetFileInfo.absolutePath()); + + QFile sourceFile(source); + if (!sourceFile.copy(target)) { + qDebug() << "failed!\n"; + throw QInstaller::Error(QString::fromLatin1("Cannot copy the %1 file from \"%2\" to \"%3\": " + "%4").arg(kind, QDir::toNativeSeparators(source), QDir::toNativeSeparators(target), + /* in case of an existing target the error String does not show the file */ + (targetFileInfo.exists() ? QLatin1String("Target already exist.") : sourceFile.errorString()))); + } + + qDebug() << "done."; +} + +static QStringList copyFilesFromNode(const QString &parentNode, const QString &childNode, const QString &attr, + const QString &kind, const QDomNode &package, const PackageInfo &info, const QString &targetDir) +{ + QStringList copiedFiles; + const QDomNodeList nodes = package.firstChildElement(parentNode).childNodes(); + for (int i = 0; i < nodes.count(); ++i) { + const QDomNode node = nodes.at(i); + if (node.nodeName() != childNode) + continue; + + const QDir dir(QString::fromLatin1("%1/meta").arg(info.directory)); + const QString filter = attr.isEmpty() ? node.toElement().text() : node.toElement().attribute(attr); + const QStringList files = dir.entryList(QStringList(filter), QDir::Files); + if (files.isEmpty()) { + throw QInstaller::Error(QString::fromLatin1("Cannot find any %1 matching \"%2\" " + "while copying %1 of \"%3\".").arg(kind, filter, info.name)); + } + + foreach (const QString &file, files) { + const QString source(QString::fromLatin1("%1/meta/%2").arg(info.directory, file)); + const QString target(QString::fromLatin1("%1/%2/%3").arg(targetDir, info.name, file)); + copyWithException(source, target, kind); + copiedFiles.append(file); + } + } + return copiedFiles; +} + +void QInstallerTools::copyMetaData(const QString &_targetDir, const QString &metaDataDir, + const PackageInfoVector &packages, const QString &appName, const QString &appVersion, + const QStringList &uniteMetadatas) +{ + const QString targetDir = makePathAbsolute(_targetDir); + if (!QFile::exists(targetDir)) + QInstaller::mkpath(targetDir); + + bool componentMetaExtracted = false; + QDomDocument doc; + QDomElement root; + QFile existingUpdatesXml(QFileInfo(metaDataDir, QLatin1String("Updates.xml")).absoluteFilePath()); + if (existingUpdatesXml.open(QIODevice::ReadOnly) && doc.setContent(&existingUpdatesXml)) { + root = doc.documentElement(); + // remove entry for this component from existing Updates.xml, if found + foreach (const PackageInfo &info, packages) { + const QDomNodeList packageNodes = root.childNodes(); + for (int i = packageNodes.count() - 1; i >= 0; --i) { + const QDomNode node = packageNodes.at(i); + if (node.nodeName() != QLatin1String("PackageUpdate")) + continue; + if (node.firstChildElement(QLatin1String("Name")).text() != info.name) + continue; + root.removeChild(node); + } + } + existingUpdatesXml.close(); + } else { + root = doc.createElement(QLatin1String("Updates")); + root.appendChild(doc.createElement(QLatin1String("ApplicationName"))).appendChild(doc + .createTextNode(appName)); + root.appendChild(doc.createElement(QLatin1String("ApplicationVersion"))).appendChild(doc + .createTextNode(appVersion)); + root.appendChild(doc.createElement(QLatin1String("Checksum"))).appendChild(doc + .createTextNode(QLatin1String("true"))); + } + + foreach (const PackageInfo &info, packages) { + if (info.metaFile.isEmpty() && info.metaNode.isEmpty()) { + if (!QDir(targetDir).mkpath(info.name)) + throw QInstaller::Error(QString::fromLatin1("Cannot create directory \"%1\".").arg(info.name)); + + const QString packageXmlPath = QString::fromLatin1("%1/meta/package.xml").arg(info.directory); + qDebug() << "Copy meta data for package" << info.name << "using" << packageXmlPath; + + QFile file(packageXmlPath); + QInstaller::openForRead(&file); + + QString errMsg; + int line = 0; + int column = 0; + QDomDocument packageXml; + if (!packageXml.setContent(&file, &errMsg, &line, &column)) { + throw QInstaller::Error(QString::fromLatin1("Cannot parse \"%1\": line: %2, column: %3: %4 (%5)") + .arg(QDir::toNativeSeparators(packageXmlPath)).arg(line).arg(column).arg(errMsg, info.name)); + } + + QDomElement update = doc.createElement(QLatin1String("PackageUpdate")); + QDomNode nameElement = update.appendChild(doc.createElement(QLatin1String("Name"))); + nameElement.appendChild(doc.createTextNode(info.name)); + + // list of current unused or later transformed tags + QStringList blackList; + blackList << QLatin1String("UserInterfaces") << QLatin1String("Translations") << + QLatin1String("Licenses") << QLatin1String("Name"); + + bool foundDefault = false; + bool foundVirtual = false; + bool foundDisplayName = false; + bool foundDownloadableArchives = false; + bool foundCheckable = false; + const QDomNode package = packageXml.firstChildElement(QLatin1String("Package")); + const QDomNodeList childNodes = package.childNodes(); + for (int i = 0; i < childNodes.count(); ++i) { + const QDomNode node = childNodes.at(i); + const QString key = node.nodeName(); + + if (key == QLatin1String("Default")) + foundDefault = true; + if (key == QLatin1String("Virtual")) + foundVirtual = true; + if (key == QLatin1String("DisplayName")) + foundDisplayName = true; + if (key == QLatin1String("DownloadableArchives")) + foundDownloadableArchives = true; + if (key == QLatin1String("Checkable")) + foundCheckable = true; + if (node.isComment() || blackList.contains(key)) + continue; // just skip comments and some tags... + + QDomElement element = doc.createElement(key); + for (int j = 0; j < node.attributes().size(); ++j) { + element.setAttribute(node.attributes().item(j).toAttr().name(), + node.attributes().item(j).toAttr().value()); + } + update.appendChild(element).appendChild(doc.createTextNode(node.toElement().text())); + } + + if (foundDefault && foundVirtual) { + throw QInstaller::Error(QString::fromLatin1("Error: <Default> and <Virtual> elements are " + "mutually exclusive in file \"%1\".").arg(QDir::toNativeSeparators(packageXmlPath))); + } + + if (foundDefault && foundCheckable) { + throw QInstaller::Error(QString::fromLatin1("Error: <Default> and <Checkable>" + "elements are mutually exclusive in file \"%1\".") + .arg(QDir::toNativeSeparators(packageXmlPath))); + } + + if (!foundDisplayName) { + qWarning() << "No DisplayName tag found at" << info.name << ", using component Name instead."; + QDomElement displayNameElement = doc.createElement(QLatin1String("DisplayName")); + update.appendChild(displayNameElement).appendChild(doc.createTextNode(info.name)); + } + + // get the size of the data + quint64 componentSize = 0; + quint64 compressedComponentSize = 0; + + const QDir::Filters filters = QDir::Files | QDir::NoDotAndDotDot; + const QDir dataDir = QString::fromLatin1("%1/%2/data").arg(metaDataDir, info.name); + const QFileInfoList entries = dataDir.exists() ? dataDir.entryInfoList(filters | QDir::Dirs) + : QDir(QString::fromLatin1("%1/%2").arg(metaDataDir, info.name)).entryInfoList(filters); + qDebug() << "calculate size of directory" << dataDir.absolutePath(); + foreach (const QFileInfo &fi, entries) { + try { + if (fi.isDir()) { + QDirIterator recursDirIt(fi.filePath(), QDirIterator::Subdirectories); + while (recursDirIt.hasNext()) { + recursDirIt.next(); + const quint64 size = QInstaller::fileSize(recursDirIt.fileInfo()); + componentSize += size; + compressedComponentSize += size; + } + } else if (Lib7z::isSupportedArchive(fi.filePath())) { + // if it's an archive already, list its files and sum the uncompressed sizes + QFile archive(fi.filePath()); + compressedComponentSize += archive.size(); + QInstaller::openForRead(&archive); + + QVector<Lib7z::File>::const_iterator fileIt; + const QVector<Lib7z::File> files = Lib7z::listArchive(&archive); + for (fileIt = files.begin(); fileIt != files.end(); ++fileIt) + componentSize += fileIt->uncompressedSize; + } else { + // otherwise just add its size + const quint64 size = QInstaller::fileSize(fi); + componentSize += size; + compressedComponentSize += size; + } + } catch (const QInstaller::Error &error) { + qDebug().noquote() << error.message(); + } catch(...) { + // ignore, that's just about the sizes - and size doesn't matter, you know? + } + } + + QDomElement fileElement = doc.createElement(QLatin1String("UpdateFile")); + fileElement.setAttribute(QLatin1String("UncompressedSize"), componentSize); + fileElement.setAttribute(QLatin1String("CompressedSize"), compressedComponentSize); + // adding the OS attribute to be compatible with old sdks + fileElement.setAttribute(QLatin1String("OS"), QLatin1String("Any")); + update.appendChild(fileElement); + + root.appendChild(update); + + // copy script file + const QString script = package.firstChildElement(QLatin1String("Script")).text(); + if (!script.isEmpty()) { + QFile scriptFile(QString::fromLatin1("%1/meta/%2").arg(info.directory, script)); + if (!scriptFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + throw QInstaller::Error(QString::fromLatin1("Cannot open component script at \"%1\".") + .arg(QDir::toNativeSeparators(scriptFile.fileName()))); + } + + const QString scriptContent = QLatin1String("(function() {") + + QString::fromUtf8(scriptFile.readAll()) + + QLatin1String(";" + " if (typeof Component == \"undefined\")" + " throw \"Missing Component constructor. Please check your script.\";" + "})();"); + + // if the user isn't aware of the downloadable archives value we will add it automatically later + foundDownloadableArchives |= scriptContent.contains(QLatin1String("addDownloadableArchive")) + || scriptContent.contains(QLatin1String("removeDownloadableArchive")); + + static QInstaller::ScriptEngine testScriptEngine; + const QJSValue value = testScriptEngine.evaluate(scriptContent, scriptFile.fileName()); + if (value.isError()) { + throw QInstaller::Error(QString::fromLatin1("Exception while loading component " + "script at \"%1\": %2").arg(QDir::toNativeSeparators(scriptFile.fileName()), + value.toString().isEmpty() ? QString::fromLatin1("Unknown error.") : + value.toString() + QStringLiteral(" on line number: ") + + value.property(QStringLiteral("lineNumber")).toString())); + } + + const QString toLocation(QString::fromLatin1("%1/%2/%3").arg(targetDir, info.name, script)); + copyWithException(scriptFile.fileName(), toLocation, QInstaller::scScript); + } + + // write DownloadableArchives tag if that is missed by the user + if (!foundDownloadableArchives && !info.copiedFiles.isEmpty()) { + QStringList realContentFiles; + foreach (const QString &filePath, info.copiedFiles) { + if (!filePath.endsWith(QLatin1String(".sha1"), Qt::CaseInsensitive)) { + const QString fileName = QFileInfo(filePath).fileName(); + // remove unnecessary version string from filename and add it to the list + realContentFiles.append(fileName.mid(info.version.count())); + } + } + + update.appendChild(doc.createElement(QLatin1String("DownloadableArchives"))).appendChild(doc + .createTextNode(realContentFiles.join(QChar::fromLatin1(',')))); + } + + // copy user interfaces + const QStringList uiFiles = copyFilesFromNode(QLatin1String("UserInterfaces"), + QLatin1String("UserInterface"), QString(), QLatin1String("user interface"), package, info, + targetDir); + if (!uiFiles.isEmpty()) { + update.appendChild(doc.createElement(QLatin1String("UserInterfaces"))).appendChild(doc + .createTextNode(uiFiles.join(QChar::fromLatin1(',')))); + } + + // copy translations + QStringList trFiles; + if (!qApp->arguments().contains(QString::fromLatin1("--ignore-translations"))) { + trFiles = copyFilesFromNode(QLatin1String("Translations"), QLatin1String("Translation"), + QString(), QLatin1String("translation"), package, info, targetDir); + if (!trFiles.isEmpty()) { + update.appendChild(doc.createElement(QLatin1String("Translations"))).appendChild(doc + .createTextNode(trFiles.join(QChar::fromLatin1(',')))); + } + } + + // copy license files + const QStringList licenses = copyFilesFromNode(QLatin1String("Licenses"), QLatin1String("License"), + QLatin1String("file"), QLatin1String("license"), package, info, targetDir); + if (!licenses.isEmpty()) { + foreach (const QString &trFile, trFiles) { + // Copy translated license file based on the assumption that it will have the same base name + // as the original license plus the file name of an existing translation file without suffix. + foreach (const QString &license, licenses) { + const QFileInfo untranslated(license); + const QString translatedLicense = QString::fromLatin1("%2_%3.%4").arg(untranslated + .baseName(), QFileInfo(trFile).baseName(), untranslated.completeSuffix()); + // ignore copy failure, that's just about the translations + QFile::copy(QString::fromLatin1("%1/meta/%2").arg(info.directory).arg(translatedLicense), + QString::fromLatin1("%1/%2/%3").arg(targetDir, info.name, translatedLicense)); + } + } + update.appendChild(package.firstChildElement(QLatin1String("Licenses")).cloneNode()); + } + } else { + // Extract metadata from archive + if (!info.metaFile.isEmpty()){ + QFile metaFile(info.metaFile); + QInstaller::openForRead(&metaFile); + Lib7z::extractArchive(&metaFile, targetDir); + componentMetaExtracted = true; + } + + // Restore "PackageUpdate" node; + QDomDocument update; + if (!update.setContent(info.metaNode)) { + throw QInstaller::Error(QString::fromLatin1("Cannot restore \"PackageUpdate\" description for node %1").arg(info.name)); + } + + root.appendChild(update.documentElement()); + } + } + + if (!componentMetaExtracted) { + foreach (const QString uniteMetadata, uniteMetadatas) { + QFile metaFile(QFileInfo(metaDataDir, uniteMetadata).absoluteFilePath()); + QInstaller::openForRead(&metaFile); + Lib7z::extractArchive(&metaFile, targetDir); + } + } + + doc.appendChild(root); + + QFile targetUpdatesXml(targetDir + QLatin1String("/Updates.xml")); + QInstaller::openForWrite(&targetUpdatesXml); + QInstaller::blockingWrite(&targetUpdatesXml, doc.toByteArray()); +} + +PackageInfoVector QInstallerTools::createListOfPackages(const QStringList &packagesDirectories, + QStringList *packagesToFilter, FilterType filterType) +{ + qDebug() << "Collecting information about available packages..."; + + bool ignoreInvalidPackages = qApp->arguments().contains(QString::fromLatin1("--ignore-invalid-packages")); + + PackageInfoVector dict; + QFileInfoList entries; + foreach (const QString &packagesDirectory, packagesDirectories) + entries.append(QDir(packagesDirectory).entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot)); + for (QFileInfoList::const_iterator it = entries.constBegin(); it != entries.constEnd(); ++it) { + if (filterType == Exclude) { + // Check for current file in exclude list, if found, skip it and remove it from exclude list + if (packagesToFilter->contains(it->fileName())) { + packagesToFilter->removeAll(it->fileName()); + continue; + } + } else { + // Check for current file in include list, if not found, skip it; if found, remove it from include list + if (!packagesToFilter->contains(it->fileName())) + continue; + packagesToFilter->removeAll(it->fileName()); + } + qDebug() << "Found subdirectory" << it->fileName(); + // because the filter is QDir::Dirs - filename means the name of the subdirectory + if (it->fileName().contains(QLatin1Char('-'))) { + qDebug("When using the component \"%s\" as a dependency, " + "to ensure backward compatibility, you must add a colon symbol at the end, " + "even if you do not specify a version.", + qUtf8Printable(it->fileName())); + } + + QFile file(QString::fromLatin1("%1/meta/package.xml").arg(it->filePath())); + QFileInfo fileInfo(file); + if (!fileInfo.exists()) { + if (ignoreInvalidPackages) + continue; + throw QInstaller::Error(QString::fromLatin1("Component \"%1\" does not contain a package " + "description (meta/package.xml is missing).").arg(QDir::toNativeSeparators(it->fileName()))); + } + + file.open(QIODevice::ReadOnly); + + QDomDocument doc; + QString error; + int errorLine = 0; + int errorColumn = 0; + if (!doc.setContent(&file, &error, &errorLine, &errorColumn)) { + if (ignoreInvalidPackages) + continue; + throw QInstaller::Error(QString::fromLatin1("Component package description in \"%1\" is invalid. " + "Error at line: %2, column: %3 -> %4").arg(QDir::toNativeSeparators(fileInfo.absoluteFilePath()), + QString::number(errorLine), + QString::number(errorColumn), error)); + } + + const QDomElement packageElement = doc.firstChildElement(QLatin1String("Package")); + const QString name = packageElement.firstChildElement(QLatin1String("Name")).text(); + if (!name.isEmpty() && name != it->fileName()) { + qWarning().nospace() << "The <Name> tag in the file " << fileInfo.absoluteFilePath() + << " is ignored - the installer uses the path element right before the 'meta'" + << " (" << it->fileName() << ")"; + } + + QString releaseDate = packageElement.firstChildElement(QLatin1String("ReleaseDate")).text(); + if (releaseDate.isEmpty()) { + qWarning("Release date for \"%s\" is empty! Using the current date instead.", + qPrintable(fileInfo.absoluteFilePath())); + releaseDate = QDate::currentDate().toString(Qt::ISODate); + } + + if (!QDate::fromString(releaseDate, Qt::ISODate).isValid()) { + if (ignoreInvalidPackages) + continue; + throw QInstaller::Error(QString::fromLatin1("Release date for \"%1\" is invalid! <ReleaseDate>%2" + "</ReleaseDate>. Supported format: YYYY-MM-DD").arg(QDir::toNativeSeparators(fileInfo.absoluteFilePath()), + releaseDate)); + } + + PackageInfo info; + info.name = it->fileName(); + info.version = packageElement.firstChildElement(QLatin1String("Version")).text(); + // Version cannot start with comparison characters, be an empty string + // or have whitespaces at the beginning or at the end + if (!QRegExp(QLatin1String("(?![<=>\\s]+)(.+)")).exactMatch(info.version) || + (info.version != info.version.trimmed())) { + if (ignoreInvalidPackages) + continue; + throw QInstaller::Error(QString::fromLatin1("Component version for \"%1\" is invalid! <Version>%2</Version>") + .arg(QDir::toNativeSeparators(fileInfo.absoluteFilePath()), info.version)); + } + info.dependencies = packageElement.firstChildElement(QLatin1String("Dependencies")).text() + .split(QInstaller::commaRegExp(), QString::SkipEmptyParts); + info.directory = it->filePath(); + dict.push_back(info); + + qDebug() << "- it provides the package" << info.name << " - " << info.version; + } + + if (!packagesToFilter->isEmpty() && packagesToFilter->at(0) != QString::fromLatin1( + "X_fake_filter_component_for_online_only_installer_X")) { + qWarning() << "The following explicitly given packages could not be found\n in package directory:" << *packagesToFilter; + } + + if (dict.isEmpty()) + qDebug() << "No available packages found at the specified location."; + + return dict; +} + +PackageInfoVector QInstallerTools::createListOfRepositoryPackages(const QStringList &repositoryDirectories, + QStringList *packagesToFilter, FilterType filterType) +{ + qDebug() << "Collecting information about available repository packages..."; + + bool ignoreInvalidRepositories = qApp->arguments().contains(QString::fromLatin1("--ignore-invalid-repositories")); + + PackageInfoVector dict; + QFileInfoList entries; + foreach (const QString &repositoryDirectory, repositoryDirectories) + entries.append(QFileInfo(repositoryDirectory)); + for (QFileInfoList::const_iterator it = entries.constBegin(); it != entries.constEnd(); ++it) { + + qDebug() << "Process repository" << it->fileName(); + + QFile file(QString::fromLatin1("%1/Updates.xml").arg(it->filePath())); + + QFileInfo fileInfo(file); + if (!fileInfo.exists()) { + if (ignoreInvalidRepositories) { + qDebug() << "- skip invalid repository"; + continue; + } + throw QInstaller::Error(QString::fromLatin1("Repository \"%1\" does not contain a update " + "description (Updates.xml is missing).").arg(QDir::toNativeSeparators(it->fileName()))); + } + if (!file.open(QIODevice::ReadOnly)) { + qDebug() << "Cannot open Updates.xml for reading:" << file.errorString(); + continue; + } + + QString error; + QDomDocument doc; + if (!doc.setContent(&file, &error)) { + qDebug().nospace() << "Cannot fetch a valid version of Updates.xml from repository " + << it->fileName() << ": " << error; + continue; + } + file.close(); + + const QDomElement root = doc.documentElement(); + if (root.tagName() != QLatin1String("Updates")) { + throw QInstaller::Error(QCoreApplication::translate("QInstaller", + "Invalid content in \"%1\".").arg(QDir::toNativeSeparators(file.fileName()))); + } + + const QDomNodeList children = root.childNodes(); + for (int i = 0; i < children.count(); ++i) { + const QDomElement el = children.at(i).toElement(); + if ((!el.isNull()) && (el.tagName() == QLatin1String("PackageUpdate"))) { + QInstallerTools::PackageInfo info; + + QDomElement c1 = el.firstChildElement(QInstaller::scName); + if (!c1.isNull()) + info.name = c1.text(); + else + continue; + if (filterType == Exclude) { + // Check for current package in exclude list, if found, skip it + if (packagesToFilter->contains(info.name)) { + continue; + } + } else { + // Check for current package in include list, if not found, skip it + if (!packagesToFilter->contains(info.name)) + continue; + } + c1 = el.firstChildElement(QInstaller::scVersion); + if (!c1.isNull()) + info.version = c1.text(); + else + continue; + + info.directory = QString::fromLatin1("%1/%2").arg(it->filePath(), info.name); + const QDomElement sha1 = el.firstChildElement(QInstaller::scSHA1); + if (!sha1.isNull()) { + info.metaFile = QString::fromLatin1("%1/%3%2").arg(info.directory, + QString::fromLatin1("meta.7z"), info.version); + } + + const QDomNodeList c2 = el.childNodes(); + for (int j = 0; j < c2.count(); ++j) { + const QDomElement c2Element = c2.at(j).toElement(); + if (c2Element.tagName() == QInstaller::scDependencies) + info.dependencies = c2Element.text() + .split(QInstaller::commaRegExp(), QString::SkipEmptyParts); + else if (c2Element.tagName() == QInstaller::scDownloadableArchives) { + QStringList names = c2Element.text() + .split(QInstaller::commaRegExp(), QString::SkipEmptyParts); + foreach (const QString &name, names) { + info.copiedFiles.append(QString::fromLatin1("%1/%3%2").arg(info.directory, + name, info.version)); + info.copiedFiles.append(QString::fromLatin1("%1/%3%2.sha1").arg(info.directory, + name, info.version)); + } + } + } + QString metaString; + { + QTextStream metaStream(&metaString); + el.save(metaStream, 0); + } + info.metaNode = metaString; + dict.push_back(info); + qDebug() << "- it provides the package" << info.name << " - " << info.version; + } + } + } + + return dict; +} + +QHash<QString, QString> QInstallerTools::buildPathToVersionMapping(const PackageInfoVector &info) +{ + QHash<QString, QString> map; + foreach (const PackageInfo &inf, info) + map[inf.name] = inf.version; + return map; +} + +static void writeSHA1ToNodeWithName(QDomDocument &doc, QDomNodeList &list, const QByteArray &sha1sum, + const QString &nodename = QString()) +{ + if (nodename.isEmpty()) + qDebug() << "Writing sha1sum node."; + else + qDebug() << "Searching sha1sum node for" << nodename; + QString sha1Value = QString::fromLatin1(sha1sum.toHex().constData()); + for (int i = 0; i < list.size(); ++i) { + QDomNode curNode = list.at(i); + QDomNode nameTag = curNode.firstChildElement(scName); + if ((!nameTag.isNull() && nameTag.toElement().text() == nodename) || nodename.isEmpty()) { + QDomNode sha1Node = curNode.firstChildElement(scSHA1); + QDomNode newSha1Node = doc.createElement(scSHA1); + newSha1Node.appendChild(doc.createTextNode(sha1Value)); + + if (!sha1Node.isNull() && sha1Node.hasChildNodes()) { + QDomNode sha1NodeChild = sha1Node.firstChild(); + QString sha1OldValue = sha1NodeChild.nodeValue(); + if (sha1Value == sha1OldValue) { + qDebug() << "- keeping the existing sha1sum" << sha1OldValue; + continue; + } else { + qDebug() << "- clearing the old sha1sum" << sha1OldValue; + sha1Node.removeChild(sha1NodeChild); + } + } + if (sha1Node.isNull()) + curNode.appendChild(newSha1Node); + else + curNode.replaceChild(newSha1Node, sha1Node); + qDebug() << "- writing the sha1sum" << sha1Value; + } + } +} + +void QInstallerTools::compressMetaDirectories(const QString &repoDir, const QString &existingUnite7zUrl, + const QHash<QString, QString> &versionMapping, bool createSplitMetadata, bool createUnifiedMetadata) +{ + QDomDocument doc; + // use existing Updates.xml, if any + QFile existingUpdatesXml(QFileInfo(QDir(repoDir), QLatin1String("Updates.xml")).absoluteFilePath()); + if (!existingUpdatesXml.open(QIODevice::ReadOnly) || !doc.setContent(&existingUpdatesXml)) { + qDebug() << "Cannot find Updates.xml"; + } + existingUpdatesXml.close(); + + QDir dir(repoDir); + const QStringList entryList = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + + QStringList absPaths; + if (createUnifiedMetadata) { + absPaths = unifyMetadata(repoDir, existingUnite7zUrl, doc); + } + + if (createSplitMetadata) { + splitMetadata(entryList, repoDir, doc, versionMapping); + } else { + // remove the files that got compressed + foreach (const QString path, absPaths) + QInstaller::removeFiles(path, true); + } + + QInstaller::openForWrite(&existingUpdatesXml); + QInstaller::blockingWrite(&existingUpdatesXml, doc.toByteArray()); + existingUpdatesXml.close(); +} + +QStringList QInstallerTools::unifyMetadata(const QString &repoDir, const QString &existingRepoDir, QDomDocument doc) +{ + QStringList absPaths; + QDir dir(repoDir); + const QStringList entryList = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + + foreach (const QString &i, entryList) { + dir.cd(i); + const QString absPath = dir.absolutePath(); + absPaths.append(absPath); + dir.cdUp(); + } + + QTemporaryDir existingRepoTempDir; + QString existingRepoTemp = existingRepoTempDir.path(); + if (!existingRepoDir.isEmpty()) { + existingRepoTempDir.setAutoRemove(false); + QFile archiveFile(existingRepoDir); + QInstaller::openForRead(&archiveFile); + Lib7z::extractArchive(&archiveFile, existingRepoTemp); + QDir dir(existingRepoTemp); + QStringList existingRepoEntries = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + foreach (const QString existingRepoEntry, existingRepoEntries) { + if (entryList.contains(existingRepoEntry)) { + continue; + } else { + dir.cd(existingRepoEntry); + const QString absPath = dir.absolutePath(); + absPaths.append(absPath); + } + } + } + + // Compress all metadata from repository to one single 7z + const QString metadataFilename = QDateTime::currentDateTime(). + toString(QLatin1String("yyyy-MM-dd-hhmm")) + QLatin1String("_meta.7z"); + const QString tmpTarget = repoDir + QDir::separator() + metadataFilename; + Lib7z::createArchive(tmpTarget, absPaths, Lib7z::TmpFile::No); + + QFile tmp(tmpTarget); + tmp.open(QFile::ReadOnly); + const QByteArray sha1Sum = QInstaller::calculateHash(&tmp, QCryptographicHash::Sha1); + QDomNodeList elements = doc.elementsByTagName(QLatin1String("Updates")); + writeSHA1ToNodeWithName(doc, elements, sha1Sum, QString()); + + qDebug() << "Updating the metadata node with name " << metadataFilename; + if (elements.count() > 0) { + QDomNode node = elements.at(0); + QDomNode nameTag = node.firstChildElement(QLatin1String("MetadataName")); + + QDomNode newNodeTag = doc.createElement(QLatin1String("MetadataName")); + newNodeTag.appendChild(doc.createTextNode(metadataFilename)); + + if (nameTag.isNull()) + node.appendChild(newNodeTag); + else + node.replaceChild(newNodeTag, nameTag); + } + QInstaller::removeDirectory(existingRepoTemp, true); + return absPaths; +} + +void QInstallerTools::splitMetadata(const QStringList &entryList, const QString &repoDir, + QDomDocument doc, const QHash<QString, QString> &versionMapping) +{ + QStringList absPaths; + QDomNodeList elements = doc.elementsByTagName(QLatin1String("PackageUpdate")); + QDir dir(repoDir); + foreach (const QString &i, entryList) { + dir.cd(i); + const QString absPath = dir.absolutePath(); + const QString path = QString(i).remove(repoDir); + if (path.isNull()) + continue; + const QString versionPrefix = versionMapping[path]; + const QString fn = QLatin1String(versionPrefix.toLatin1() + "meta.7z"); + const QString tmpTarget = repoDir + QLatin1String("/") + fn; + Lib7z::createArchive(tmpTarget, QStringList() << absPath, Lib7z::TmpFile::No); + // remove the files that got compressed + QInstaller::removeFiles(absPath, true); + QFile tmp(tmpTarget); + tmp.open(QFile::ReadOnly); + const QByteArray sha1Sum = QInstaller::calculateHash(&tmp, QCryptographicHash::Sha1); + writeSHA1ToNodeWithName(doc, elements, sha1Sum, path); + const QString finalTarget = absPath + QLatin1String("/") + fn; + if (!tmp.rename(finalTarget)) { + throw QInstaller::Error(QString::fromLatin1("Cannot move file \"%1\" to \"%2\".").arg( + QDir::toNativeSeparators(tmpTarget), QDir::toNativeSeparators(finalTarget))); + } + dir.cdUp(); + } +} + +void QInstallerTools::copyComponentData(const QStringList &packageDirs, const QString &repoDir, + PackageInfoVector *const infos) +{ + for (int i = 0; i < infos->count(); ++i) { + const PackageInfo info = infos->at(i); + const QString name = info.name; + qDebug() << "Copying component data for" << name; + + const QString namedRepoDir = QString::fromLatin1("%1/%2").arg(repoDir, name); + if (!QDir().mkpath(namedRepoDir)) { + throw QInstaller::Error(QString::fromLatin1("Cannot create repository directory for component \"%1\".") + .arg(name)); + } + + if (info.copiedFiles.isEmpty()) { + QStringList compressedFiles; + QStringList filesToCompress; + foreach (const QString &packageDir, packageDirs) { + const QDir dataDir(QString::fromLatin1("%1/%2/data").arg(packageDir, name)); + foreach (const QString &entry, dataDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::Files)) { + QFileInfo fileInfo(dataDir.absoluteFilePath(entry)); + if (fileInfo.isFile() && !fileInfo.isSymLink()) { + const QString absoluteEntryFilePath = dataDir.absoluteFilePath(entry); + if (Lib7z::isSupportedArchive(absoluteEntryFilePath)) { + QFile tmp(absoluteEntryFilePath); + QString target = QString::fromLatin1("%1/%3%2").arg(namedRepoDir, entry, info.version); + qDebug() << "Copying archive from" << tmp.fileName() << "to" << target; + if (!tmp.copy(target)) { + throw QInstaller::Error(QString::fromLatin1("Cannot copy file \"%1\" to \"%2\": %3") + .arg(QDir::toNativeSeparators(tmp.fileName()), QDir::toNativeSeparators(target), tmp.errorString())); + } + compressedFiles.append(target); + } else { + filesToCompress.append(absoluteEntryFilePath); + } + } else if (fileInfo.isDir()) { + qDebug() << "Compressing data directory" << entry; + QString target = QString::fromLatin1("%1/%3%2.7z").arg(namedRepoDir, entry, info.version); + Lib7z::createArchive(target, QStringList() << dataDir.absoluteFilePath(entry), + Lib7z::TmpFile::No); + compressedFiles.append(target); + } else if (fileInfo.isSymLink()) { + filesToCompress.append(dataDir.absoluteFilePath(entry)); + } + } + } + + if (!filesToCompress.isEmpty()) { + qDebug() << "Compressing files found in data directory:" << filesToCompress; + QString target = QString::fromLatin1("%1/%3%2").arg(namedRepoDir, QLatin1String("content.7z"), + info.version); + Lib7z::createArchive(target, filesToCompress, Lib7z::TmpFile::No); + compressedFiles.append(target); + } + + foreach (const QString &target, compressedFiles) { + (*infos)[i].copiedFiles.append(target); + + QFile archiveFile(target); + QFile archiveHashFile(archiveFile.fileName() + QLatin1String(".sha1")); + + qDebug() << "Hash is stored in" << archiveHashFile.fileName(); + qDebug() << "Creating hash of archive" << archiveFile.fileName(); + + try { + QInstaller::openForRead(&archiveFile); + const QByteArray hashOfArchiveData = QInstaller::calculateHash(&archiveFile, + QCryptographicHash::Sha1).toHex(); + archiveFile.close(); + + QInstaller::openForWrite(&archiveHashFile); + archiveHashFile.write(hashOfArchiveData); + qDebug() << "Generated sha1 hash:" << hashOfArchiveData; + (*infos)[i].copiedFiles.append(archiveHashFile.fileName()); + archiveHashFile.close(); + } catch (const QInstaller::Error &/*e*/) { + archiveFile.close(); + archiveHashFile.close(); + throw; + } + } + } else { + foreach (const QString &file, (*infos)[i].copiedFiles) { + QFileInfo fromInfo(file); + QFile from(file); + QString target = QString::fromLatin1("%1/%2").arg(namedRepoDir, fromInfo.fileName()); + qDebug() << "Copying file from" << from.fileName() << "to" << target; + if (!from.copy(target)) { + throw QInstaller::Error(QString::fromLatin1("Cannot copy file \"%1\" to \"%2\": %3") + .arg(QDir::toNativeSeparators(from.fileName()), QDir::toNativeSeparators(target), from.errorString())); + } + } + } + } +} + +void QInstallerTools::filterNewComponents(const QString &repositoryDir, QInstallerTools::PackageInfoVector &packages) +{ + QDomDocument doc; + QFile file(repositoryDir + QLatin1String("/Updates.xml")); + if (file.open(QFile::ReadOnly) && doc.setContent(&file)) { + const QDomElement root = doc.documentElement(); + if (root.tagName() != QLatin1String("Updates")) { + throw QInstaller::Error(QCoreApplication::translate("QInstaller", + "Invalid content in \"%1\".").arg(QDir::toNativeSeparators(file.fileName()))); + } + file.close(); // close the file, we read the content already + + // read the already existing updates xml content + const QDomNodeList children = root.childNodes(); + QHash <QString, QInstallerTools::PackageInfo> hash; + for (int i = 0; i < children.count(); ++i) { + const QDomElement el = children.at(i).toElement(); + if ((!el.isNull()) && (el.tagName() == QLatin1String("PackageUpdate"))) { + QInstallerTools::PackageInfo info; + const QDomNodeList c2 = el.childNodes(); + for (int j = 0; j < c2.count(); ++j) { + const QDomElement c2Element = c2.at(j).toElement(); + if (c2Element.tagName() == scName) + info.name = c2Element.text(); + else if (c2Element.tagName() == scVersion) + info.version = c2Element.text(); + } + hash.insert(info.name, info); + } + } + + // remove all components that have no update (decision based on the version tag) + for (int i = packages.count() - 1; i >= 0; --i) { + const QInstallerTools::PackageInfo info = packages.at(i); + + // check if component already exists & version did not change + if (hash.contains(info.name) && KDUpdater::compareVersion(info.version, hash.value(info.name).version) < 1) { + packages.remove(i); // the version did not change, no need to update the component + continue; + } + qDebug() << "Update component" << info.name << "in"<< repositoryDir << "."; + } + } +} + +QString QInstallerTools::existingUniteMeta7z(const QString &repositoryDir) +{ + QString uniteMeta7z = QString(); + QFile file(repositoryDir + QLatin1String("/Updates.xml")); + QDomDocument doc; + if (file.open(QFile::ReadOnly) && doc.setContent(&file)) { + QDomNodeList elements = doc.elementsByTagName(QLatin1String("MetadataName")); + if (elements.count() > 0 && elements.at(0).isElement()) { + uniteMeta7z = elements.at(0).toElement().text(); + QFile metaFile(repositoryDir + QDir::separator() + uniteMeta7z); + if (!metaFile.exists()) { + throw QInstaller::Error(QString::fromLatin1("Unite meta7z \"%1\" does not exist in repository \"%2\"") + .arg(QDir::toNativeSeparators(metaFile.fileName()), repositoryDir)); + } + } + } + return uniteMeta7z; +} diff --git a/src/libs/ifwtools/repositorygen.h b/src/libs/ifwtools/repositorygen.h new file mode 100644 index 000000000..49d0a51dd --- /dev/null +++ b/src/libs/ifwtools/repositorygen.h @@ -0,0 +1,88 @@ +/************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the Qt Installer Framework. +** +** $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$ +** +**************************************************************************/ + +#ifndef REPOSITORYGEN_H +#define REPOSITORYGEN_H + +#include "ifwtools_global.h" + +#include <QHash> +#include <QString> +#include <QStringList> +#include <QVector> +#include <QDomDocument> + +namespace QInstallerTools { + +struct IFWTOOLS_EXPORT PackageInfo +{ + QString name; + QString version; + QString directory; + QStringList dependencies; + QStringList copiedFiles; + QString metaFile; + QString metaNode; +}; +typedef QVector<PackageInfo> PackageInfoVector; + +enum IFWTOOLS_EXPORT FilterType { + Include, + Exclude +}; + +void IFWTOOLS_EXPORT printRepositoryGenOptions(); +QString IFWTOOLS_EXPORT makePathAbsolute(const QString &path); +void IFWTOOLS_EXPORT copyWithException(const QString &source, const QString &target, const QString &kind = QString()); + +PackageInfoVector IFWTOOLS_EXPORT createListOfPackages(const QStringList &packagesDirectories, QStringList *packagesToFilter, + FilterType ftype); + +PackageInfoVector IFWTOOLS_EXPORT createListOfRepositoryPackages(const QStringList &repositoryDirectories, QStringList *packagesToFilter, + FilterType filterType); + +QHash<QString, QString> IFWTOOLS_EXPORT buildPathToVersionMapping(const PackageInfoVector &info); + +void IFWTOOLS_EXPORT compressMetaDirectories(const QString &repoDir, const QString &existingUnite7zUrl, + const QHash<QString, QString> &versionMapping, bool createSplitMetadata, bool createUnifiedMetadata); + +QStringList unifyMetadata(const QString &repoDir, const QString &existingRepoDir, QDomDocument doc); +void splitMetadata(const QStringList &entryList, const QString &repoDir, QDomDocument doc, + const QHash<QString, QString> &versionMapping); + +void IFWTOOLS_EXPORT copyMetaData(const QString &outDir, const QString &dataDir, const PackageInfoVector &packages, + const QString &appName, const QString& appVersion, const QStringList &uniteMetadatas); +void IFWTOOLS_EXPORT copyComponentData(const QStringList &packageDir, const QString &repoDir, PackageInfoVector *const infos); + +void IFWTOOLS_EXPORT filterNewComponents(const QString &repositoryDir, QInstallerTools::PackageInfoVector &packages); + +QString IFWTOOLS_EXPORT existingUniteMeta7z(const QString &repositoryDir); + +} // namespace QInstallerTools + +#endif // REPOSITORYGEN_H diff --git a/src/libs/ifwtools/resources/copylibsintobundle.sh b/src/libs/ifwtools/resources/copylibsintobundle.sh new file mode 100644 index 000000000..1ad5da4db --- /dev/null +++ b/src/libs/ifwtools/resources/copylibsintobundle.sh @@ -0,0 +1,189 @@ +#!/bin/sh +############################################################################# +## +## Copyright (C) 2017 The Qt Company Ltd. +## Contact: https://www.qt.io/licensing/ +## +## This file is part of the Qt Installer Framework. +## +## $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$ +## +############################################################################# + + +# this script puts all libs directly needed by the bundle into it + +QTDIR="" +IS_DEBUG=0 +HAVE_CORE=0 +HAVE_SVG=0 +HAVE_PHONON=0 +HAVE_SCRIPT=0 +HAVE_SQL=0 +HAVE_WEBKIT=0 + +function handleFile() +{ + local FILE=$1 + local BUNDLE=$2 + + # all dynamic libs directly needed by the bundle, which are not in /System/Library or in /usr/lib (which are system default libs, which we don't want) + local LIBS=`xcrun otool -L $FILE | grep -v 'executable_path' | grep -v '/System/Library' | grep -v '/usr/lib' | grep '/' | sed -ne 's,^ *\(.*\) (.*,\1,p'` + + local lib + for lib in $LIBS; do + local NAME=`basename $lib` + + if echo $NAME | grep 'QtCore' >/dev/null; then + HAVE_CORE=1 + QTDIR=`echo $lib | sed -ne 's,^\(.*\)/lib/[^/]*QtCore.*$,\1,p'` + if echo $NAME | grep 'debug' >/dev/null; then + IS_DEBUG=1 + fi + elif echo $NAME | grep 'QtSvg' >/dev/null; then + HAVE_SVG=1 + elif echo $NAME | grep 'phonon' >/dev/null; then + HAVE_PHONON=1 + elif echo $NAME | grep 'QtScript' >/dev/null; then + HAVE_SCRIPT=1 + elif echo $NAME | grep 'QtSql' >/dev/null; then + HAVE_SQL=1 + elif echo $NAME | grep 'QtWebKit' >/dev/null; then + HAVE_WEBKIT=1 + fi + + if [ `basename $FILE` != $NAME ]; then + + # this part handles libraries which are macOS frameworks + if echo $lib | grep '\.framework' >/dev/null; then + local FRAMEWORKPATH=`echo $lib | sed -ne 's,\(.*\.framework\).*,\1,p'` + local FRAMEWORKNAME=`basename $FRAMEWORKPATH` + local NEWFRAMEWORKPATH=`echo $lib | sed -ne "s,.*\($FRAMEWORKNAME\),\1,p"` + + # Qt installed via the precompled binaries... + if [ $FRAMEWORKPATH = $FRAMEWORKNAME ]; then + FRAMEWORKPATH="/Library/Frameworks/$FRAMEWORKNAME" + if [ ! -e "$FRAMEWORKPATH" ]; then + echo "Framework $FRAMEWORKNAME not found." + exit 1 + fi + fi + + if [ ! -e "$BUNDLE/Contents/Frameworks/$NEWFRAMEWORKPATH" ]; then + echo Embedding framework $FRAMEWORKNAME + + + # copy the framework into the bundle + cp -R $FRAMEWORKPATH $BUNDLE/Contents/Frameworks + # remove debug libs we've copied + find $BUNDLE/Contents/Frameworks/$FRAMEWORKNAME -regex '.*_debug\(\.dSYM\)*' | xargs rm -rf + + handleFile "$BUNDLE/Contents/Frameworks/$NEWFRAMEWORKPATH" "$BUNDLE" + fi + # and inform the dynamic linker about this + xcrun install_name_tool -change $lib @executable_path/../Frameworks/$NEWFRAMEWORKPATH $FILE + + + # this part handles 'normal' dynamic libraries (.dylib) + else + if [ ! -e "$BUNDLE/Contents/Frameworks/$NAME" ]; then + echo Embedding library $NAME + + # Qt installed via the precompled binaries... + if [ $lib = $NAME ]; then + lib="/Library/Frameworks/$NAME" + if [ ! -e "$lib" ]; then + lib="/usr/lib/$NAME" + fi + if [ ! -e "$lib" ]; then + echo "Library $NAME not found." + exit 1 + fi + fi + + # copy the lib into the bundle + cp $lib $BUNDLE/Contents/Frameworks + handleFile "$BUNDLE/Contents/Frameworks/$NAME" "$BUNDLE" + fi + + # and inform the dynamic linker about this + xcrun install_name_tool -change $lib @executable_path/../Frameworks/$NAME $FILE + fi + + fi + done +} + +function handleQtPlugins() +{ + local PLUGINPATH=$QTDIR/plugins + + # QTDIR was not found, then we're using /Developer/Applications/Qt + if [ "$PLUGINPATH" = "/plugins" ]; then + PLUGINPATH="/Developer/Applications/Qt/plugins" + fi + + CLASS=$1 + EXECUTABLE=$2 + BUNDLE=$3 + mkdir -p $BUNDLE/Contents/plugins/$CLASS + echo Add $CLASS plugins + for plugin in `ls $PLUGINPATH/$CLASS/*`; do + plugin=`basename $plugin` + if echo $plugin | grep 'debug' >/dev/null; then + #if [ $IS_DEBUG -eq 1 ]; then + cp "$PLUGINPATH/$CLASS/$plugin" $BUNDLE/Contents/plugins/$CLASS + xcrun install_name_tool -change $plugin @executable_path/../plugins/$CLASS/$plugin $EXECUTABLE + handleFile $BUNDLE/Contents/plugins/$CLASS/$plugin $BUNDLE + #fi + else + #if [ $IS_DEBUG -eq 0 ]; then + cp "$PLUGINPATH/$CLASS/$plugin" $BUNDLE/Contents/plugins/$CLASS + xcrun install_name_tool -change $plugin @executable_path/../plugins/$CLASS/$plugin $EXECUTABLE + handleFile $BUNDLE/Contents/plugins/$CLASS/$plugin $BUNDLE + #fi + fi + done +} + +# the app bundle we're working with +BUNDLE=$1 +# the executable inside of the bundle +EXECUTABLE=$BUNDLE/Contents/MacOS/`xargs < $BUNDLE/Contents/Info.plist | sed -ne 's,.*<key>CFBundleExecutable</key> <string>\([^<]*\)</string>.*,\1,p'` + +mkdir -p $BUNDLE/Contents/Frameworks + +handleFile $EXECUTABLE $BUNDLE + +if [ $HAVE_CORE -eq 1 ]; then + handleQtPlugins "imageformats" "$EXECUTABLE" "$BUNDLE" +fi +if [ $HAVE_SVG -eq 1 ]; then + handleQtPlugins "iconengines" "$EXECUTABLE" "$BUNDLE" +fi +if [ $HAVE_PHONON -eq 1 ]; then + handleQtPlugins "phonon_backend" "$EXECUTABLE" "$BUNDLE" +fi +if [ $HAVE_SQL -eq 1 ]; then + handleQtPlugins "sqldrivers" "$EXECUTABLE" "$BUNDLE" +fi +if [ $HAVE_WEBKIT -eq 1 ]; then + handleQtPlugins "codecs" "$EXECUTABLE" "$BUNDLE" +fi diff --git a/src/libs/ifwtools/resources/default_icon_mac.icns b/src/libs/ifwtools/resources/default_icon_mac.icns Binary files differnew file mode 100644 index 000000000..8d870d649 --- /dev/null +++ b/src/libs/ifwtools/resources/default_icon_mac.icns diff --git a/src/libs/ifwtools/resources/ifwtools.qrc b/src/libs/ifwtools/resources/ifwtools.qrc new file mode 100644 index 000000000..8b73a8442 --- /dev/null +++ b/src/libs/ifwtools/resources/ifwtools.qrc @@ -0,0 +1,7 @@ +<!DOCTYPE RCC><RCC version="1.0"> +<qresource> + <file>default_icon_mac.icns</file> + <file>copylibsintobundle.sh</file> + <file alias="resources/installerbase.ico">../../../sdk/installerbase.ico</file> +</qresource> +</RCC> diff --git a/src/libs/installer/installer.pro b/src/libs/installer/installer.pro index d1e21bb63..2462dbe63 100644 --- a/src/libs/installer/installer.pro +++ b/src/libs/installer/installer.pro @@ -6,6 +6,7 @@ CONFIG += staticlib include(../7zip/7zip.pri) include(../kdtools/kdtools.pri) +include(../ifwtools/ifwtools.pri) include(../../../installerfw.pri) # productkeycheck API |