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/libs/ifwtools/repositorygen.cpp | |
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/libs/ifwtools/repositorygen.cpp')
-rw-r--r-- | src/libs/ifwtools/repositorygen.cpp | 966 |
1 files changed, 966 insertions, 0 deletions
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; +} |