diff options
Diffstat (limited to 'src/libs/installer/componentalias.cpp')
-rw-r--r-- | src/libs/installer/componentalias.cpp | 649 |
1 files changed, 649 insertions, 0 deletions
diff --git a/src/libs/installer/componentalias.cpp b/src/libs/installer/componentalias.cpp new file mode 100644 index 000000000..955d715fd --- /dev/null +++ b/src/libs/installer/componentalias.cpp @@ -0,0 +1,649 @@ +/************************************************************************** +** +** Copyright (C) 2023 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 "componentalias.h" + +#include "constants.h" +#include "globals.h" +#include "packagemanagercore.h" +#include "updater.h" + +#include <QJsonDocument> +#include <QJsonObject> +#include <QJsonArray> + +namespace QInstaller { + +static const QStringList scPossibleElements { + scName, + scDisplayName, + scDescription, + scVersion, + scVirtual, + scRequiredComponents, + scRequiredAliases, + scOptionalComponents, + scOptionalAliases, + scReleaseDate +}; + +/*! + \inmodule QtInstallerFramework + \class QInstaller::AliasSource + \brief Describes a source for alias declarations. +*/ + +/*! + \enum QInstaller::AliasSource::SourceFileFormat + + This enum type holds the possible file formats for alias source: + + \value Unknown + Invalid or unknown file format. + \value Xml + XML file format. + \value Json + JSON file format. +*/ + +/*! + Constructs an alias source with empty information. +*/ +AliasSource::AliasSource() + : priority(-1) +{} + +/*! + Constructs an alias source with source file format \a aFormat, filename \a aFilename, and priority \a aPriority. +*/ +AliasSource::AliasSource(SourceFileFormat aFormat, const QString &aFilename, int aPriority) + : format(aFormat) + , filename(aFilename) + , priority(aPriority) +{} + +/*! + Copy-constructs an alias source from \a other. +*/ +AliasSource::AliasSource(const AliasSource &other) +{ + format = other.format; + filename = other.filename; + priority = other.priority; +} + +/*! + Returns the hash value for the \a key, using \a seed to seed the calculation. +*/ +hashValue qHash(const AliasSource &key, hashValue seed) +{ + return qHash(key.filename, seed) ^ key.priority; +} + +/*! + Returns \c true if \a lhs and \a rhs are equal; otherwise returns \c false. +*/ +bool operator==(const AliasSource &lhs, const AliasSource &rhs) +{ + return lhs.filename == rhs.filename && lhs.priority == rhs.priority && lhs.format == rhs.format; +} + +/*! + \inmodule QtInstallerFramework + \class QInstaller::AliasFinder + \brief Creates component alias objects from parsed alias source files, based + on version and source priorities. +*/ + +/*! + Constructs a new alias finder with \a core as the package manager instance. +*/ +AliasFinder::AliasFinder(PackageManagerCore *core) + : m_core(core) +{ +} + +/*! + Destroys the finder and cleans unreleased results. +*/ +AliasFinder::~AliasFinder() +{ + clear(); +} + +/*! + Runs the finder. Parses the alias source files and creates component alias + objects based on the parsed data. Same alias may be declared in multiple source + files, thus source priority and version information is used to decide which + source is used for creating the alias object. + + Any previous results are cleared when calling this. + + Returns \c true if at least one alias was found, \c false otherwise. +*/ +bool AliasFinder::run() +{ + clear(); + + if (m_sources.isEmpty()) + return false; + + // 1. Parse source files + for (auto &source : qAsConst(m_sources)) { + if (source.format == AliasSource::SourceFileFormat::Unknown) { + qCWarning(QInstaller::lcInstallerInstallLog) + << "Unknown alias source format for file:" << source.filename; + continue; + } + if (source.format == AliasSource::SourceFileFormat::Xml) + parseXml(source); + else if (source.format == AliasSource::SourceFileFormat::Json) + parseJson(source); + } + + // 2. Create aliases based on priority & version + for (auto &data : qAsConst(m_aliasData)) { + const QString name = data.value(scName).toString(); + const Resolution resolution = checkPriorityAndVersion(data); + if (resolution == Resolution::KeepExisting) + continue; + + if (resolution == Resolution::RemoveExisting) + delete m_aliases.take(name); + + ComponentAlias *alias = new ComponentAlias(m_core); + AliasData::const_iterator it; + for (it = data.cbegin(); it != data.cend(); ++it) { + if (it.value().canConvert<QString>()) + alias->setValue(it.key(), it.value().toString()); + } + m_aliases.insert(name, alias); + } + + return !m_aliases.isEmpty(); +} + +/*! + Returns a list of the found aliases. +*/ +QList<ComponentAlias *> AliasFinder::aliases() const +{ + return m_aliases.values(); +} + +/*! + Sets the alias sources to look alias information from to \a sources. +*/ +void AliasFinder::setAliasSources(const QSet<AliasSource> &sources) +{ + clear(); + m_sources = sources; +} + +/*! + Clears the results of the finder. +*/ +void AliasFinder::clear() +{ + qDeleteAll(m_aliases); + + m_aliases.clear(); + m_aliasData.clear(); +} + +/*! + Reads an XML file specified by \a filename, and constructs a variant map of + the data for each alias. + + Returns \c true on success, \c false otherwise. +*/ +bool AliasFinder::parseXml(AliasSource source) +{ + QFile file(source.filename); + if (!file.open(QIODevice::ReadOnly)) { + qCWarning(QInstaller::lcInstallerInstallLog) + << "Cannot open alias definition for reading:" << file.errorString(); + return false; + } + + QString error; + int errorLine; + int errorColumn; + + QDomDocument doc; + if (!doc.setContent(&file, &error, &errorLine, &errorColumn)) { + qCWarning(QInstaller::lcInstallerInstallLog) + << "Cannot read alias definition document:" << error + << "line:" << errorLine << "column:" << errorColumn; + return false; + } + file.close(); + + const QDomElement root = doc.documentElement(); + const QDomNodeList children = root.childNodes(); + + for (int i = 0; i < children.count(); ++i) { + const QDomElement el = children.at(i).toElement(); + const QString tag = el.tagName(); + if (el.isNull() || tag != scAlias) { + qCWarning(lcInstallerInstallLog) << "Unexpected element name:" << tag; + continue; + } + + AliasData data; + data.insert(QLatin1String("source"), QVariant::fromValue(source)); + + const QDomNodeList c2 = el.childNodes(); + for (int j = 0; j < c2.count(); ++j) { + const QDomElement el2 = c2.at(j).toElement(); + const QString tag2 = el2.tagName(); + if (!scPossibleElements.contains(tag2)) { + qCWarning(lcInstallerInstallLog) << "Unexpected element name:" << tag2; + continue; + } + data.insert(tag2, el2.text()); + } + + m_aliasData.insert(data.value(scName).toString(), data); + } + + return true; +} + +/*! + Reads a JSON file specified by \a source, and constructs a variant map of + the data for each alias. + + Returns \c true on success, \c false otherwise. +*/ +bool AliasFinder::parseJson(AliasSource source) +{ + QFile file(source.filename); + if (!file.open(QIODevice::ReadOnly)) { + qCWarning(QInstaller::lcInstallerInstallLog) + << "Cannot open alias definition for reading:" << file.errorString(); + return false; + } + + const QByteArray jsonData = file.readAll(); + const QJsonDocument doc(QJsonDocument::fromJson(jsonData)); + const QJsonObject docJsonObject = doc.object(); + + const QJsonArray aliases = docJsonObject.value(QLatin1String("alias-packages")).toArray(); + for (auto &it : aliases) { + AliasData data; + data.insert(QLatin1String("source"), QVariant::fromValue(source)); + + QJsonObject aliasObj = it.toObject(); + for (const auto &key : aliasObj.keys()) { + if (!scPossibleElements.contains(key)) { + qCWarning(lcInstallerInstallLog) << "Unexpected element name:" << key; + continue; + } + + const QJsonValue jsonValue = aliasObj.value(key); + if (key == scRequiredComponents || key == scRequiredAliases + || key == scOptionalComponents || key == scOptionalAliases) { + const QJsonArray requirements = jsonValue.toArray(); + QString requiresString; + + for (const auto &it2 : requirements) { + requiresString.append(it2.toString()); + if (it2 != requirements.last()) + requiresString.append(QLatin1Char(',')); + } + + data.insert(key, requiresString); + } else if (key == scVirtual) { + data.insert(key, QVariant(jsonValue.toBool()))->toString(); + } else { + data.insert(key, jsonValue.toString()); + } + } + + m_aliasData.insert(data.value(scName).toString(), data); + } + + return true; +} + +/*! + Checks whether \a data should be used for creating a new alias object, + based on version and source priority. + + If an alias of the same name exists, always use the one with the higher + version. If the new alias has the same version but a higher + priority, use the new new alias. Otherwise keep the already existing alias. + + Returns the resolution of the check. +*/ +AliasFinder::Resolution AliasFinder::checkPriorityAndVersion(const AliasData &data) const +{ + for (const auto &existingData : m_aliasData.values(data.value(scName).toString())) { + if (existingData == data) + continue; + + const int versionMatch = KDUpdater::compareVersion(data.value(scVersion).toString(), + existingData.value(scVersion).toString()); + + const AliasSource newSource = data.value(QLatin1String("source")).value<AliasSource>(); + const AliasSource oldSource = existingData.value(QLatin1String("source")).value<AliasSource>(); + + if (versionMatch > 0) { + // new alias has higher version, use + qCDebug(QInstaller::lcDeveloperBuild).nospace() << "Remove Alias 'Name: " + << data.value(scName).toString() << ", Version: " << existingData.value(scVersion).toString() + << ", Source: " << oldSource.filename + << "' found an alias with higher version 'Name: " + << data.value(scName).toString() << ", Version: " << data.value(scVersion).toString() + << ", Source: " << newSource.filename << "'"; + + return Resolution::RemoveExisting; + } + + if ((versionMatch == 0) && (newSource.priority > oldSource.priority)) { + // new alias version equals but priority is higher, use + qCDebug(QInstaller::lcDeveloperBuild).nospace() << "Remove Alias 'Name: " + << data.value(scName).toString() << ", Priority: " << oldSource.priority + << ", Source: " << oldSource.filename + << "' found an alias with higher priority 'Name: " + << data.value(scName).toString() << ", Priority: " << newSource.priority + << ", Source: " << newSource.filename << "'"; + + return Resolution::RemoveExisting; + } + + return Resolution::KeepExisting; // otherwise keep existing + } + + return Resolution::AddNew; +} + +/*! + \inmodule QtInstallerFramework + \class QInstaller::ComponentAlias + \brief The ComponentAlias class represents an alias for single or multiple components. +*/ + +/*! + \enum QInstaller::ComponentAlias::UnstableError + + This enum type holds the possible reasons for marking an alias unstable: + + \value ReferenceToUnstable + Alias requires another alias that is marked unstable. + \value MissingComponent + Alias requires a component that is missing. + \value UnselectableComponent + Alias requires a component that cannot be selected. + \value MissingAlias + Alias requires another alias that is missing. + \value ComponentNameConfict + Alias has a name that conflicts with a name of a component +*/ + +/*! + Constructs a new component alias with \a core as the package manager instance. +*/ +ComponentAlias::ComponentAlias(PackageManagerCore *core) + : m_core(core) + , m_selected(false) + , m_unstable(false) +{ +} + +/*! + Destructs the alias. +*/ +ComponentAlias::~ComponentAlias() +{ +} + +/*! + Returns the name of the alias. +*/ +QString ComponentAlias::name() const +{ + return m_variables.value(scName); +} + +/*! + Returns the display name of the alias. +*/ +QString ComponentAlias::displayName() const +{ + return m_variables.value(scDisplayName); +} + +/*! + Returns the description text of the alias. +*/ +QString ComponentAlias::description() const +{ + return m_variables.value(scDescription); +} + +/*! + Returns the version of the alias. +*/ +QString ComponentAlias::version() const +{ + return m_variables.value(scVersion); +} + +/*! + Returns \c true if the alias is virtual, \c false otherwise. + + Virtual aliases are aliases that cannot be selected by the + user, and are invisible. They can be required by other aliases however. +*/ +bool ComponentAlias::isVirtual() const +{ + return m_variables.value(scVirtual, scFalse).toLower() == scTrue; +} + +/*! + Returns \c true if the alias is selected for installation, \c false otherwise. +*/ +bool ComponentAlias::isSelected() const +{ + return m_selected; +} + +/*! + Sets the selection state of the alias to \a selected. The selection + does not have an effect if the alias is unselectable. +*/ +void ComponentAlias::setSelected(bool selected) +{ + if (selected && (isUnstable() || isVirtual())) + return; + + m_selected = selected; +} + +/*! + Returns the list of components required by this alias, or an + empty list if this alias does not require any components. +*/ +QList<Component *> ComponentAlias::components() +{ + if (m_components.isEmpty()) { + const QStringList componentList = QInstaller::splitStringWithComma( + m_variables.value(scRequiredComponents)); + + const QStringList optionalComponentList = QInstaller::splitStringWithComma( + m_variables.value(scOptionalComponents)); + + addRequiredComponents(componentList, false); + addRequiredComponents(optionalComponentList, true); + } + + return m_components; +} + +/*! + Returns the list of other aliases required by this alias, or an + empty list if this alias does not require any other aliases. +*/ +QList<ComponentAlias *> ComponentAlias::aliases() +{ + if (m_aliases.isEmpty()) { + const QStringList aliasList = QInstaller::splitStringWithComma( + m_variables.value(scRequiredAliases)); + + const QStringList optionalAliasList = QInstaller::splitStringWithComma( + m_variables.value(scOptionalAliases)); + + addRequiredAliases(aliasList, false); + addRequiredAliases(optionalAliasList, true); + } + + return m_aliases; +} + +/*! + Returns the value specified by \a key, with an optional default value \a defaultValue. +*/ +QString ComponentAlias::value(const QString &key, const QString &defaultValue) const +{ + return m_variables.value(key, defaultValue); +} + +/*! + Sets the value specified by \a key to \a value. If the value exists already, + it is replaced with the new value. +*/ +void ComponentAlias::setValue(const QString &key, const QString &value) +{ + const QString normalizedValue = m_core->replaceVariables(value); + if (m_variables.value(key) == normalizedValue) + return; + + m_variables[key] = normalizedValue; +} + +/*! + Returns all keys for the component alias values. +*/ +QStringList ComponentAlias::keys() const +{ + return m_variables.keys(); +} + +/*! + Returns \c true if the alias is marked unstable, \c false otherwise. +*/ +bool ComponentAlias::isUnstable() const +{ + return m_unstable; +} + +/*! + Sets the alias unstable with \a error, and a \a message describing the error. +*/ +void ComponentAlias::setUnstable(UnstableError error, const QString &message) +{ + setSelected(false); + m_unstable = true; + + const QMetaEnum metaEnum = QMetaEnum::fromType<ComponentAlias::UnstableError>(); + emit m_core->unstableComponentFound( + QLatin1String(metaEnum.valueToKey(error)), message, name()); +} + +/*! + \internal + + Adds the \a aliases to the list of required aliases by this alias. If \a optional + is \c true, missing alias references are ignored. +*/ +void ComponentAlias::addRequiredAliases(const QStringList &aliases, const bool optional) +{ + for (const auto &aliasName : aliases) { + ComponentAlias *alias = m_core->aliasByName(aliasName); + if (!alias) { + if (optional) + continue; + + const QString error = QLatin1String("No required alias found by name: ") + aliasName; + qCWarning(lcInstallerInstallLog) << error; + + setUnstable(UnstableError::MissingAlias, error); + continue; + } + + if (alias->isUnstable()) { + const QString error = QLatin1String("Alias requires another alias " + "that is marked unstable: ") + aliasName; + qCWarning(lcInstallerInstallLog) << error; + + setUnstable(UnstableError::ReferenceToUnstable, error); + continue; + } + + m_aliases.append(alias); + } +} + +/*! + \internal + + Adds the \a components to the list of required components by this alias. If \a optional + is \c true, missing component references are ignored. +*/ +void ComponentAlias::addRequiredComponents(const QStringList &components, const bool optional) +{ + for (const auto &componentName : components) { + Component *component = m_core->componentByName(componentName); + if (!component) { + if (optional) + continue; + + const QString error = QLatin1String("No required component found by name: ") + + componentName; + qCWarning(lcInstallerInstallLog) << error; + + setUnstable(UnstableError::MissingComponent, error); + continue; + } + + if (component->isUnstable() || !component->isCheckable()) { + const QString error = QLatin1String("Alias requires component that is uncheckable or unstable: ") + + componentName; + qCWarning(lcInstallerInstallLog) << error; + + setUnstable(UnstableError::UnselectableComponent, error); + continue; + } + + m_components.append(component); + } +} + +} // namespace QInstaller |