mirror of
https://pagure.io/fm-orchestrator.git
synced 2026-02-02 20:59:06 +08:00
Most of the code are moved to dedicated subpackages, but some others can't due to the cycle dependencies. Signed-off-by: Chenxiong Qi <cqi@redhat.com>
634 lines
32 KiB
Python
634 lines
32 KiB
Python
# -*- coding: utf-8 -*-
|
|
# SPDX-License-Identifier: MIT
|
|
from __future__ import absolute_import
|
|
import collections
|
|
import itertools
|
|
|
|
import solv
|
|
|
|
from module_build_service.common import log, conf, models
|
|
|
|
|
|
class MMDResolver(object):
|
|
"""
|
|
Resolves dependencies between Module metadata objects.
|
|
"""
|
|
|
|
def module_dep(self, name, stream=None, version=None, version_op=None):
|
|
"""Create a libsolv Dependency
|
|
|
|
Dependency could be in following forms:
|
|
|
|
module(name)
|
|
module(name:stream)
|
|
module(name:stream) op version
|
|
|
|
:param str name: module name.
|
|
:param str stream: optional module stream. If specified, dependency
|
|
will be the 2nd form above.
|
|
:param str version: optional module version.
|
|
:param version_op: optional libsolv relational flag constant. If
|
|
specified, dependency will be the 3rd form above. Defaults to
|
|
``solv.REL_EQ``.
|
|
:return: a libsolv Dependency object
|
|
"""
|
|
if name and stream:
|
|
dep = self.pool.Dep("module({}:{})".format(name, stream))
|
|
else:
|
|
dep = self.pool.Dep("module({})".format(name))
|
|
if version:
|
|
dep = dep.Rel(version_op or solv.REL_EQ, self.pool.Dep(version))
|
|
return dep
|
|
|
|
def solvable_provides(self, solvable, name, stream=None, version=None, version_op=None):
|
|
"""Add a Provides: dependency to a solvable
|
|
|
|
This is parallel to RPM-world ``Provides: perl(foo)`` or ``Requires: perl(foo)``.
|
|
|
|
Please refer to :meth:`module_dep` for detailed information of
|
|
arguments name, stream, version and version_op.
|
|
|
|
:param solvable: a solvable object the Provides dependency will be
|
|
added to.
|
|
"""
|
|
dep = self.module_dep(name, stream, version, version_op)
|
|
solvable.add_deparray(solv.SOLVABLE_PROVIDES, dep)
|
|
|
|
def __init__(self):
|
|
self.pool = solv.Pool()
|
|
self.pool.setarch("x86_64")
|
|
self.build_repo = self.pool.add_repo("build")
|
|
self.available_repo = self.pool.add_repo("available")
|
|
# Solvable objects representing modules stored in a list grouped by
|
|
# the name:stream.
|
|
self.solvables = {}
|
|
|
|
def _deps2reqs(self, deps, base_module_stream_overrides=None, exact_versions=True):
|
|
"""
|
|
Helper method converting dependencies from MMD to solv.Dep instance expressing
|
|
the dependencies in a way libsolv accepts as input.
|
|
|
|
So for example for following input:
|
|
deps = [{'gtk': ['1'], 'foo': ['1']}]
|
|
The resulting solv.Dep expression will be:
|
|
((module(gtk) with module(gtk:1)) and (module(foo) with module(foo:1)))
|
|
|
|
Base modules are handled in a special way in case when the stream of base module
|
|
contains version in the "x.y.z" format. For example "el8.0.0" or "el7.6.0".
|
|
In this case, the resulting solv.Dep expression for such base module will contain version
|
|
string computed using ModuleBuild.get_stream_version() method:
|
|
For example:
|
|
module(platform) with module(platform:el8) = 080200
|
|
|
|
The stream used to compute the version can be also overridden using the
|
|
`base_module_stream_overrides` dict which has base module name as a key and
|
|
the stream which will be used to compute the version as a value.
|
|
This is needed for cases when module requires just "platform:el8", but was
|
|
in fact built against particular platform stream, for example platform:el8.1.0.
|
|
In this case, such module should still require platform:el8, but in particular
|
|
version which is passed to this method using the `base_module_stream_overrides`.
|
|
|
|
When `exact_versions` is set to False, the base module dependency will contain
|
|
">=" operator instead of "=".
|
|
|
|
The "with" syntax is here to allow depending on "module(gtk)" meaning "any gtk".
|
|
This can happen in case {'gtk': []} is used as an input.
|
|
|
|
See the inline comments for more information.
|
|
|
|
:param list deps: List of dicts with dependency name as key and list of
|
|
streams as value.
|
|
:param dict base_module_stream_overrides: The key is base module name, value
|
|
is the stream string which will be used to compute `version` part of the
|
|
base module solv.Dep expression.
|
|
:param bool exact_versions: When set to False, the base module dependency
|
|
will contain ">=" operator instead of "=".
|
|
:rtype: solv.Dep
|
|
:return: solv.Dep instance with dependencies in form libsolv accepts.
|
|
"""
|
|
|
|
# There are relations between modules in `deps`. For example:
|
|
# deps = [{'gtk': ['1'], 'foo': ['1']}]" means "gtk:1 and foo:1" are both required.
|
|
# deps = [{'gtk': ['1', '2']}"] means "gtk:1 or gtk:2" are required.
|
|
# This method helps creating such relations using following syntax:
|
|
# rel_or_dep(solv.Dep, solve.REL_OR, stream_dep(name, stream))
|
|
# rel_or_dep(solv.Dep, solve.REL_AND, stream_dep(name, stream))
|
|
# rel_or_dep(solv.Dep, solve.REL_WITH, stream_dep(name, stream))
|
|
# rel_or_dep(solv.Dep, solve.REL_WITHOUT, stream_dep(name, stream))
|
|
rel_or_dep = lambda dep, op, rel: dep.Rel(op, rel) if dep is not None else rel
|
|
|
|
# Check each dependency dict in `deps` list and generate the solv requirements.
|
|
reqs = None
|
|
for dep_dicts in deps:
|
|
# Contains the solv.Dep requirements for current dict.
|
|
require = None
|
|
for name, streams in dep_dicts.items():
|
|
is_base_module = name in conf.base_module_names
|
|
|
|
# The req_pos will store solv.Dep expression for "positive" requirements.
|
|
# That is the case of 'gtk': ['1', '2'].
|
|
req_pos = None
|
|
|
|
# For each stream in `streams` for this dependency, generate the
|
|
# module(name:stream) solv.Dep and add REL_OR relations between them.
|
|
for stream in streams:
|
|
if is_base_module:
|
|
# Override the stream which is used to compute the stream version in case
|
|
# `base_module_stream_overrides` is set.
|
|
if base_module_stream_overrides and name in base_module_stream_overrides:
|
|
stream_for_version = base_module_stream_overrides[name]
|
|
else:
|
|
stream_for_version = stream
|
|
|
|
# In case x.y.z versioning is not used for this base module, do not
|
|
# use versions solv.Dep.
|
|
stream_version_str = str(
|
|
models.ModuleBuild.get_stream_version(
|
|
stream_for_version, right_pad=False))
|
|
if len(stream_version_str) < 5:
|
|
req_pos = rel_or_dep(
|
|
req_pos, solv.REL_OR, self.module_dep(name, stream))
|
|
else:
|
|
# The main reason why to use `exact_versions` is the case when
|
|
# adding deps for the input module we want to resolve. This module
|
|
# buildrequires exact stream version of base module against which it
|
|
# needs for building and we should never pull in different one.
|
|
# But for modules which are buildrequires of this input module, we
|
|
# want to use "base_module >= stream_version" relation, because they
|
|
# can be chery-picked even when newer base module stream_version is
|
|
# requested, for example:
|
|
# - foo buildrequires bar and also buildrequires platform:el8 = 080100.
|
|
# - bar:1 is built against platform:el8.0.0.
|
|
# - bar:2 is built against platform:el8.2.0.
|
|
# We need libsolv to allow chery-picking "bar:1", and ignore "bar:2",
|
|
# because it is built against newer platform stream version than the
|
|
# requested and and such newer version can be incompatible with the
|
|
# old one. so we express bar's dependencies on platform like this:
|
|
# - bar:1 buildrequires platform:el8 >= 080000.
|
|
# - bar:2 buildrequires platform:el8 >= 080200.
|
|
# Because the "foo" limits the solving to platform:el8 = 080100,
|
|
# the bar:2 won't be returned by libsolv, because 080100 < 080200.
|
|
# But that bar:1 will be returned by libsovl, because it buildrequires
|
|
# platform 080000 which is lesser than 080100.
|
|
op = solv.REL_EQ
|
|
if not exact_versions:
|
|
op |= solv.REL_GT
|
|
version = models.ModuleBuild.get_stream_version(
|
|
stream_for_version, right_pad=False
|
|
)
|
|
req_pos = rel_or_dep(
|
|
req_pos,
|
|
solv.REL_OR,
|
|
self.module_dep(name, stream, str(version), op)
|
|
)
|
|
else:
|
|
req_pos = rel_or_dep(req_pos, solv.REL_OR, self.module_dep(name, stream))
|
|
|
|
# Generate the module(name) solv.Dep.
|
|
req = self.module_dep(name)
|
|
|
|
if req_pos is not None:
|
|
req = req.Rel(solv.REL_WITH, req_pos)
|
|
|
|
# And in the end use AND between the last name:[streams] and the current one.
|
|
require = rel_or_dep(require, solv.REL_AND, req)
|
|
|
|
# There might be multiple dicts in `deps` list, so use OR relation between them.
|
|
reqs = rel_or_dep(reqs, solv.REL_OR, require)
|
|
|
|
return reqs
|
|
|
|
def _add_base_module_provides(self, solvable, mmd):
|
|
"""
|
|
Adds the "stream version" and the "virtual_streams" from XMD section of `mmd` to `solvable`.
|
|
|
|
Base modules like "platform" can contain virtual streams which need to be considered
|
|
when resolving dependencies. For example module "platform:el8.1.0" can provide virtual
|
|
stream "el8". In this case the solvable will have following additional Provides:
|
|
|
|
- module(platform:el8.1.0) = 80100 - Modules can require specific platform stream.
|
|
- module(platform:el8) = 80100 - Module can also require just platform:el8.
|
|
|
|
:return: A boolean that is True if a provides for the stream version was added to the input
|
|
solvable.
|
|
"""
|
|
base_stream_ver = False
|
|
|
|
if mmd.get_module_name() not in conf.base_module_names:
|
|
return base_stream_ver
|
|
|
|
# When depsolving, we will need to follow specific rules to choose the right base
|
|
# module, like sorting the base modules sharing the same virtual streams based on
|
|
# their "stream version" - For example stream "el8.1" is lower than stream "el8.2"
|
|
# and so on. We therefore need to convert the stream and version of base module to
|
|
# integer representation and add "module($name:$stream) = $stream_based_version"
|
|
# to Provides.
|
|
stream_version = models.ModuleBuild.get_stream_version(
|
|
mmd.get_stream_name(), right_pad=False)
|
|
if stream_version:
|
|
base_stream_ver = True
|
|
self.solvable_provides(
|
|
solvable, mmd.get_module_name(), mmd.get_stream_name(), str(stream_version))
|
|
|
|
xmd = mmd.get_xmd()
|
|
# Return in case virtual_streams are not set for this mmd.
|
|
if not xmd.get("mbs", {}).get("virtual_streams"):
|
|
return base_stream_ver
|
|
|
|
version = stream_version or mmd.get_version()
|
|
# For each virtual stream, add
|
|
# "module($name:$stream) = $virtual_stream_based_version" provide.
|
|
for stream in xmd["mbs"]["virtual_streams"]:
|
|
self.solvable_provides(solvable, mmd.get_module_name(), stream, str(version))
|
|
|
|
return base_stream_ver
|
|
|
|
def _get_base_module_stream_overrides(self, mmd):
|
|
"""
|
|
Checks the xmd["mbs"]["buildrequires"] and returns the dict containing
|
|
base module name as a key and stream of base module against which this
|
|
module was built. This is later used to override base module streams
|
|
in the _deps2reqs method.
|
|
|
|
:param Modulemd mmd: Metadata of module for which the stream overrides are returned.
|
|
:rtype: dict
|
|
:return: Dict with module name as a key and new stream as a value.
|
|
"""
|
|
overrides = {}
|
|
xmd = mmd.get_xmd()
|
|
if "buildrequires" in xmd.get("mbs", {}):
|
|
for base_module_name in conf.base_module_names:
|
|
if base_module_name not in xmd["mbs"]["buildrequires"]:
|
|
continue
|
|
if "stream" not in xmd["mbs"]["buildrequires"][base_module_name]:
|
|
continue
|
|
stream = xmd["mbs"]["buildrequires"][base_module_name]["stream"]
|
|
|
|
overrides[base_module_name] = stream
|
|
return overrides
|
|
|
|
def add_modules(self, mmd):
|
|
"""
|
|
Adds module represented by `mmd` metadata to MMDResolver. Modules added by this
|
|
method will be considered as possible dependencies while resolving the dependencies
|
|
using the `solve(...)` method only if their "context" is None. Otherwise they are
|
|
treated like input modules we want to resolve dependencies for.
|
|
|
|
:param Modulemd mmd: Metadata of module to add.
|
|
:rtype: list
|
|
:return: list of solv.Solvable instances representing the module in libsolv world.
|
|
"""
|
|
n, s, v, c = \
|
|
mmd.get_module_name(), mmd.get_stream_name(), mmd.get_version(), mmd.get_context()
|
|
|
|
# Helper method to return the dependencies of `mmd` in the {name: [streams], ... form}.
|
|
# The `dep_type` is either "runtime" or "buildtime" str depending on whether
|
|
# the return deps should be runtime requires or buildrequires.
|
|
normdeps = lambda mmd, dep_type: [
|
|
{
|
|
name: getattr(dep, "get_{}_streams".format(dep_type))(name)
|
|
for name in getattr(dep, "get_{}_modules".format(dep_type))()
|
|
}
|
|
for dep in mmd.get_dependencies()
|
|
]
|
|
|
|
# Each solvable object has name, version, architecture and list of
|
|
# provides/requires/conflicts which defines its relations with other solvables.
|
|
# You can imagine solvable as a single RPM.
|
|
# Single module can be represented by multiple solvables - read further inline
|
|
# comments for more info. Therefore we use list to store them.
|
|
solvables = []
|
|
if c is not None:
|
|
# If context is set, the module we are adding should be used as dependencies
|
|
# for input module. Therefore add it in "available_repo".
|
|
solvable = self.available_repo.add_solvable()
|
|
|
|
# Use n:s:v:c as name, version as version and set the arches.
|
|
solvable.name = "%s:%s:%d:%s" % (n, s, v, c)
|
|
solvable.evr = str(v)
|
|
# TODO: replace with real arch, but for now resolving using single arch
|
|
# is sufficient.
|
|
solvable.arch = "x86_64"
|
|
|
|
# Add "Provides: module(name)", each module provides itself.
|
|
# This is used for example to find the buildrequired module when
|
|
# no particular stream is used - for example when buildrequiring
|
|
# "gtk: []"
|
|
self.solvable_provides(solvable, n)
|
|
|
|
base_stream_ver = self._add_base_module_provides(solvable, mmd)
|
|
|
|
# Add "Provides: module(name:stream) = version", so we can find buildrequired
|
|
# modules when "gtk:[1]" is used and also choose the latest version.
|
|
# Skipped if this is a base module with a stream version defined.
|
|
if not base_stream_ver:
|
|
self.solvable_provides(solvable, n, s, str(v))
|
|
|
|
base_module_stream_overrides = self._get_base_module_stream_overrides(mmd)
|
|
# Fill in the "Requires" of this module, so we can track its dependencies
|
|
# on other modules.
|
|
requires = self._deps2reqs(
|
|
normdeps(mmd, "runtime"), base_module_stream_overrides, False
|
|
)
|
|
log.debug("Adding module %s with requires: %r", solvable.name, requires)
|
|
solvable.add_deparray(solv.SOLVABLE_REQUIRES, requires)
|
|
|
|
# Add "Conflicts: module(name)".
|
|
# This is needed to prevent installation of multiple streams of single module.
|
|
# For example:
|
|
# - "app:1" requires "foo:1" and "gtk:1".
|
|
# - "foo:1" requires "bar:1".
|
|
# - "gtk:1" requires "bar:2".
|
|
# "bar:1" and "bar:2" cannot be installed in the same time and therefore
|
|
# there need to be conflict defined between them.
|
|
solvable.add_deparray(solv.SOLVABLE_CONFLICTS, self.module_dep(n))
|
|
solvables.append(solvable)
|
|
|
|
# Add solvable to solvables list. Sorting is done later in the solve method.
|
|
ns = ":".join([n, s])
|
|
if ns not in self.solvables:
|
|
self.solvables[ns] = []
|
|
self.solvables[ns].append(solvable)
|
|
else:
|
|
# For input module, we might have multiple buildrequires/requires pairs in the
|
|
# input `mmd`. For example like this:
|
|
# - buildrequires:
|
|
# gtk: [1]
|
|
# platform: [f28]
|
|
# requires:
|
|
# gtk: [1]
|
|
# - buildrequires:
|
|
# gtk: [2]
|
|
# platform: [f29]
|
|
# requires:
|
|
# gtk: [2]
|
|
# This means we need: "(gtk:1 and platform:f28) or (gtk:2 and platform:f29)".
|
|
# There is no way how to express that in libsolv as single solvable and in the same
|
|
# time try all the possible combinations. Libsolv just returns the single one and does
|
|
# not offer enough data for us to tell it to try another one to really find all of
|
|
# them.
|
|
# The solution for that is therefore adding multiple solvables for each OR block of
|
|
# that input condition.
|
|
#
|
|
# So in our example, we add two solvables:
|
|
# 1) Solvable with name "n:s:v:0" and "Requires: gtk:1 and platform:f28".
|
|
# 2) Solvable with name "n:s:v:1" and "Requires: gtk:2 and platform:f29".
|
|
#
|
|
# Note the "context" field in the solvable name - it is set according to index
|
|
# of buildrequires/requires pair and uniquely identifies the Solvable.
|
|
#
|
|
# Using this trick, libsolv will try to solve all the buildrequires/requires pairs,
|
|
# because they are expressed as separate Solvables and we are able to distinguish
|
|
# between them thanks to context value.
|
|
normalized_deps = normdeps(mmd, "buildtime")
|
|
for c, deps in enumerate(mmd.get_dependencies()):
|
|
# $n:$s:$c-$v.src
|
|
solvable = self.build_repo.add_solvable()
|
|
solvable.name = "%s:%s:%d:%d" % (n, s, v, c)
|
|
solvable.evr = str(v)
|
|
solvable.arch = "src"
|
|
|
|
requires = self._deps2reqs([normalized_deps[c]])
|
|
log.debug("Adding module %s with requires: %r", solvable.name, requires)
|
|
solvable.add_deparray(solv.SOLVABLE_REQUIRES, requires)
|
|
|
|
solvables.append(solvable)
|
|
# Add solvable to solvables list. Sorting is done later in the solve method.
|
|
ns = ":".join([n, s])
|
|
if ns not in self.solvables:
|
|
self.solvables[ns] = []
|
|
self.solvables[ns].append(solvable)
|
|
|
|
return solvables
|
|
|
|
def solve(self, mmd):
|
|
"""
|
|
Solves dependencies of module defined by `mmd` object. Returns set
|
|
containing frozensets with all the possible combinations which
|
|
satisfied dependencies.
|
|
|
|
``solve`` uses a policy called "First" to resolve the dependencies.
|
|
That is, only single combination of buildrequires will be returned with
|
|
"gtk:1" and "platform:f28", because the input buildrequires section did
|
|
not mention any platform stream and therefore "first one" is used.
|
|
|
|
:param mmd: Input modulemd which should have the `context` set to None.
|
|
:type mmd: Modulemd.ModuleStream
|
|
:return: set of frozensets of n:s:v:c of modules which satisfied the
|
|
dependency solving.
|
|
"""
|
|
# Add the input module to pool and generate the "Provides" data so we can
|
|
# use them for resolving later.
|
|
solvables = self.add_modules(mmd)
|
|
if not solvables:
|
|
raise ValueError("No module(s) found for resolving")
|
|
self.pool.createwhatprovides()
|
|
|
|
# "solvable to n:s:v:c"
|
|
s2nsvca = lambda s: "%s:%s" % (s.name, s.arch)
|
|
# "solvable to n:s"
|
|
s2ns = lambda s: ":".join(s.name.split(":", 2)[:2])
|
|
|
|
# Sort the solvables by version for NSVC in descending order.
|
|
for ns, unordered_solvables in self.solvables.items():
|
|
unordered_solvables.sort(key=lambda s: int(s.name.split(":")[2]), reverse=True)
|
|
|
|
# For each solvable object generated from input module, run the solver.
|
|
# For reasons why there might be multiple solvable objects, please read the
|
|
# `add_modules(...)` inline comments.
|
|
solver = self.pool.Solver()
|
|
alternatives = collections.OrderedDict()
|
|
for src in solvables:
|
|
# Create the solv Job to represent the solving task.
|
|
job = self.pool.Job(solv.Job.SOLVER_INSTALL | solv.Job.SOLVER_SOLVABLE, src.id)
|
|
|
|
# Check that at max 1 requires element exists in the solvable object - since
|
|
# we create multiple solvable objects where each of them has at max one
|
|
# requires element, it should never be the case...
|
|
# NOTE: "requires" in solvable are actually "buildrequires" in mmd.
|
|
requires = src.lookup_deparray(solv.SOLVABLE_REQUIRES)
|
|
if len(requires) > 1:
|
|
raise SystemError("At max one element should be in Requires: %s" % requires)
|
|
elif len(requires) == 0:
|
|
# Return early in case the requires is empty, because it basically means
|
|
# the module has no buildrequires section.
|
|
return {frozenset([s2nsvca(src)])}
|
|
|
|
requires = requires[0]
|
|
src_alternatives = alternatives[src] = collections.OrderedDict()
|
|
|
|
# TODO: replace this ugliest workaround ever with sane code of parsing rich deps.
|
|
# We need to split them because whatprovides() treats "and" same way as "or" which is
|
|
# not enough to generate combinations.
|
|
# Source solvables have Req: (X and Y and Z)
|
|
# Binary solvables have Req: ((X and Y) or (X and Z))
|
|
# They do use "or" within "and", so simple string split won't work for binary packages.
|
|
if src.arch != "src":
|
|
raise NotImplementedError
|
|
|
|
# What we get in `requires` here is a string in following format:
|
|
# ((module(gtk) with module(gtk:1)) and (module(foo) with module(foo:1)) and (...))
|
|
# And what we want to get is the list of all valid combinations with particular NSVCs
|
|
# of buildrequired modules. There are few steps we need to do to achieve that:
|
|
|
|
# 1) Convert the "(R1 and R2 and R3)" string to list of solv.Dep in following format:
|
|
# [solv.Dep(R1), solv.Dep(R2), solv.Dep(R3), ...]
|
|
deps = str(requires).split(" and ")
|
|
if len(deps) > 1:
|
|
# Remove the extra parenthesis in the input string in case there are more
|
|
# rules.
|
|
deps[0] = deps[0][1:]
|
|
deps[-1] = deps[-1][:-1]
|
|
# Generate the new deps using the parserpmrichdep.
|
|
deps = [
|
|
self.pool.parserpmrichdep(dep) if dep.startswith("(") else self.pool.Dep(dep)
|
|
for dep in deps
|
|
]
|
|
|
|
# 2) For each dep (name:stream), get the set of all solvables in particular NSVCs,
|
|
# which provides that name:stream. Then use itertools.product() to actually
|
|
# generate all the possible combinations so we can try solving them.
|
|
for opt in itertools.product(*[self.pool.whatprovides(dep) for dep in deps]):
|
|
log.debug("Testing %s with combination: %s", src, opt)
|
|
# We will be trying to solve all the combinations using all the NSVCs
|
|
# we have in pool, but as we said earlier, we don't want to return
|
|
# all of them for the used resolve policy "First".
|
|
# We will achieve that by storing alternative combinations in `src_alternatives`
|
|
# with NSVC as key in case we want all of them and NS as a key when we want
|
|
# just First combination for given dependency.
|
|
# This will allow us to group alternatives for single NS in case of First
|
|
# policy and later return just the first alternative.
|
|
# `key` contains tuple similar to "('gtk:1', 'foo:1')"
|
|
key = tuple(s2ns(s) for s in opt)
|
|
|
|
# Create the solving jobs.
|
|
# We need to say to libsolv that we want it to prefer modules from the combination
|
|
# we are currently trying, otherwise it would just choose some random ones.
|
|
# We do that by FAVORING those modules - this is done in libsolv by another
|
|
# job prepending to our main job to resolve the deps of input module.
|
|
jobs = [
|
|
self.pool.Job(solv.Job.SOLVER_FAVOR | solv.Job.SOLVER_SOLVABLE, s.id)
|
|
for s in opt
|
|
] + [job]
|
|
|
|
# Log the job.
|
|
log.debug("Jobs:")
|
|
for j in jobs:
|
|
log.debug(" - %s", j)
|
|
# Solve the deps and log the dependency issues.
|
|
problems = solver.solve(jobs)
|
|
if problems:
|
|
problem_str = self._detect_transitive_stream_collision(problems)
|
|
if problem_str:
|
|
err_msg = problem_str
|
|
else:
|
|
err_msg = ", ".join(str(p) for p in problems)
|
|
raise RuntimeError(
|
|
"Problems were found during module dependency resolution: {}".format(
|
|
err_msg)
|
|
)
|
|
# Find out what was actually resolved by libsolv to be installed as a result
|
|
# of our jobs - those are the modules we are looking for.
|
|
newsolvables = solver.transaction().newsolvables()
|
|
log.debug("Transaction:")
|
|
|
|
for s in newsolvables:
|
|
log.debug(" - %s", s)
|
|
|
|
# Skip this alternative in case not all the favored Solvables are in
|
|
# the solution. For example if we favored gtk:4:1:c8, but it simply
|
|
# cannot be installed because of other dependencies, we know this is not a
|
|
# possible combination and we should not treat it as an alternative.
|
|
all_solvables_found = True
|
|
for s in opt:
|
|
if s not in newsolvables:
|
|
all_solvables_found = False
|
|
break
|
|
|
|
# Append them as an alternative for this src_alternative.
|
|
# Remember that src_alternatives are grouped by NS or NSVC depending on
|
|
# MMDResolverPolicy, so there might be more of them.
|
|
if all_solvables_found:
|
|
alternative = src_alternatives.setdefault(key, [])
|
|
alternative.append(newsolvables)
|
|
else:
|
|
log.debug(" - ^ Not all favored solvables found in the result, skipping.")
|
|
|
|
# We will check all the alternatives and keep just the "first" one.
|
|
for transactions in alternatives.values():
|
|
for ns, trans in transactions.items():
|
|
# Each transaction in trans lists all the possible working
|
|
# combination of solvables. Our goal here is to find out the
|
|
# transaction which installs the most latest Solvables - ideally
|
|
# always the latest versions of the Solvables we have, but this might
|
|
# not be always possible because of dependencies.
|
|
#
|
|
# We achieve that by generating sorted_trans list in follwing format:
|
|
# [[transaction_id, [solvable1_index, solvable2_index, ...]], [...], ...]
|
|
#
|
|
# The solvableN_index is a number saying how new the solvable is. We use
|
|
# `self.solvables` to get that number and it is simply index
|
|
# of the solvable in the particular self.solvables[name_stream] list.
|
|
# The newest solvable has therefore index 0, the next newest solvable index 1
|
|
# and so on.
|
|
#
|
|
# Then we simply sort the `sorted_trans` based on the sum of solvableN_index
|
|
# which gives us the transaction with the most recent versions. This is
|
|
# used as a solution.
|
|
sorted_trans = []
|
|
for i, t in enumerate(trans):
|
|
idx = []
|
|
for s in t:
|
|
name_stream = s2ns(s)
|
|
if name_stream not in self.solvables:
|
|
continue
|
|
index = self.solvables[name_stream].index(s)
|
|
idx.append(index)
|
|
sorted_trans.append([i, idx])
|
|
sorted_trans.sort(key=lambda i: sum(i[1]))
|
|
if sorted_trans:
|
|
transactions[ns] = [trans[sorted_trans[0][0]]]
|
|
|
|
# Convert the solvables in alternatives to nsvc and return them as set of frozensets.
|
|
return set(
|
|
frozenset(s2nsvca(s) for s in transactions[0])
|
|
for src_alternatives in alternatives.values()
|
|
for transactions in src_alternatives.values()
|
|
)
|
|
|
|
@staticmethod
|
|
def _detect_transitive_stream_collision(problems):
|
|
"""Return problem description if transitive stream collision happens
|
|
|
|
Transitive stream collision could happen if different buildrequired
|
|
modules requires same module but with different streams. For example,
|
|
|
|
app:1 --br--> gtk:1 --req--> baz:1* -------- req --------> platform:f29
|
|
| ^
|
|
+--br--> foo:1 --req--> bar:1 --req--> baz:2* --req---|
|
|
|
|
as a result, ``baz:1`` will conflicts with ``baz:2``.
|
|
|
|
:param problems: list of problems returned from ``solv.Solver.solve``.
|
|
:return: a string of problem description if transitive stream collision
|
|
is detected. The description is provided by libsolv without
|
|
changed. If no such collision, None is returned.
|
|
:rtype: str or None
|
|
"""
|
|
|
|
def find_conflicts_pairs():
|
|
for problem in problems:
|
|
for rule in problem.findallproblemrules():
|
|
info = rule.info()
|
|
if info.type == solv.Solver.SOLVER_RULE_PKG_CONFLICTS:
|
|
pair = [info.solvable.name, info.othersolvable.name]
|
|
pair.sort() # only for pretty print
|
|
yield pair
|
|
|
|
formatted_conflicts_pairs = ", ".join(
|
|
"{} and {}".format(*item) for item in find_conflicts_pairs()
|
|
)
|
|
if formatted_conflicts_pairs:
|
|
return "The module has conflicting buildrequires of: {}".format(
|
|
formatted_conflicts_pairs)
|