/************************************************************************** ** ** 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" namespace QInstaller { /*! \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. */ /*! 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); } // 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); for (auto &value : data) { if (value.canConvert()) alias->setValue(data.key(value), value.toString()); } m_aliases.insert(name, alias); } return !m_aliases.isEmpty(); } /*! Returns a list of the found aliases. */ QList AliasFinder::aliases() const { return m_aliases.values(); } /*! Sets the alias sources to look alias information from to \a sources. */ void AliasFinder::setAliasSources(const QSet &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 (tag2 != scName && tag2 != scDisplayName && tag2 != scDescription && tag2 != scVersion && tag2 != scVirtual && tag2 != scRequiredComponents && tag2 != scRequiredAliases && tag2 != scOptionalComponents && tag2 != scOptionalAliases) { qCWarning(lcInstallerInstallLog) << "Unexpected element name:" << tag2; continue; } data.insert(tag2, el2.text()); } 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(); const AliasSource oldSource = existingData.value(QLatin1String("source")).value(); 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 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::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(); 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