249 lines
8.6 KiB
Python
249 lines
8.6 KiB
Python
# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved.
|
|
#
|
|
# This file is part of kiwi.
|
|
#
|
|
# kiwi is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# kiwi is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with kiwi. If not, see <http://www.gnu.org/licenses/>
|
|
#
|
|
import importlib
|
|
import logging
|
|
from collections import namedtuple
|
|
from xml.etree import ElementTree
|
|
from xml.dom import minidom
|
|
|
|
# project
|
|
import kiwi.defaults as defaults
|
|
|
|
from kiwi.exceptions import (
|
|
KiwiSatSolverPluginError,
|
|
KiwiSatSolverJobError,
|
|
KiwiSatSolverJobProblems
|
|
)
|
|
|
|
log = logging.getLogger('kiwi')
|
|
|
|
|
|
class Sat:
|
|
"""
|
|
**Sat Solver class to run package solver operations**
|
|
|
|
The class uses SUSE's libsolv sat plugin
|
|
"""
|
|
def __init__(self):
|
|
"""
|
|
An instance of Sat auto loads the python solv plugin which is a
|
|
python binding to the libsolv C library. An exception is raised
|
|
if the module failed to load. On success a new solver pool is
|
|
initialized for this instance
|
|
|
|
:raises KiwiSatSolverPluginError: if libsolv module can't be loaded
|
|
"""
|
|
try:
|
|
self.solv = importlib.import_module('solv')
|
|
except Exception as e:
|
|
raise KiwiSatSolverPluginError(
|
|
'{0}: {1}'.format(type(e).__name__, format(e))
|
|
)
|
|
|
|
self.pool = self.solv.Pool()
|
|
self.pool.setarch()
|
|
|
|
def set_dist_type(self, dist, arch=None):
|
|
if not arch:
|
|
arch = defaults.PLATFORM_MACHINE
|
|
dist_types = {
|
|
'deb-x86_64': {
|
|
'pool_dist': self.solv.Pool.DISTTYPE_DEB,
|
|
'arch': 'amd64'
|
|
},
|
|
'deb-aarch64': {
|
|
'pool_dist': self.solv.Pool.DISTTYPE_DEB,
|
|
'arch': 'arm64'
|
|
}
|
|
}
|
|
dist_type = dist_types.get(f'{dist}-{arch}')
|
|
if dist_type:
|
|
if self.pool.setdisttype(dist_type['pool_dist']) < 0:
|
|
raise KiwiSatSolverPluginError(
|
|
f'Failed to set dist type for distribution: {dist!r}'
|
|
)
|
|
self.pool.setarch(
|
|
dist_type['arch'] if dist_type else arch
|
|
)
|
|
return dist_type
|
|
|
|
def add_repository(self, solver_repository):
|
|
"""
|
|
Add a repository solvable to the pool. This basically add the
|
|
required repository metadata which is needed to run a solver
|
|
operation later.
|
|
|
|
:param object solver_repository: Instance of :class:`SolverRepository`
|
|
"""
|
|
solvable = solver_repository.create_repository_solvable()
|
|
pool_repository = self.pool.add_repo(solver_repository.uri.uri)
|
|
pool_repository.add_solv(solvable)
|
|
self.pool.addfileprovides()
|
|
self.pool.createwhatprovides()
|
|
|
|
def solve(self, job_names, skip_missing=False, ignore_recommended=True):
|
|
"""
|
|
Solve dependencies for the given job list. The list is allowed
|
|
to contain element names of the following format:
|
|
|
|
* name
|
|
describes a package name
|
|
|
|
* pattern:name
|
|
describes a package collection name whose metadata type
|
|
is called 'pattern' and stored as such in the repository
|
|
metadata. Usually SUSE repos uses that
|
|
|
|
* group:name
|
|
describes a package collection name whose metadata type
|
|
is called 'group' and stored as such in the repository
|
|
metadata. Usually RHEL/CentOS/Fedora repos uses that
|
|
|
|
:param list job_names: list of strings
|
|
:param bool skip_missing: skip job if not found
|
|
:param bool ignore_recommended: do not include recommended packages
|
|
|
|
:raises KiwiSatSolverJobProblems: if solver reports solving problems
|
|
:return: Transaction result information
|
|
|
|
:rtype: dict
|
|
"""
|
|
solver = self.pool.Solver()
|
|
if ignore_recommended:
|
|
solver.set_flag(self.solv.Solver.SOLVER_FLAG_IGNORE_RECOMMENDED, 1)
|
|
|
|
solver_problems = solver.solve(
|
|
self._setup_jobs(job_names, skip_missing)
|
|
)
|
|
solver_problem_info = self._evaluate_solver_problems(
|
|
solver_problems
|
|
)
|
|
if solver_problem_info:
|
|
raise KiwiSatSolverJobProblems(solver_problem_info)
|
|
|
|
solver_transaction = solver.transaction()
|
|
return self._evaluate_solver_result(
|
|
solver_transaction
|
|
)
|
|
|
|
def _evaluate_solver_problems(self, solver_problems):
|
|
"""
|
|
Iterate over solver problems and their solutions
|
|
|
|
The method creates a pretty print XML information
|
|
and returns that as a UTF-8 encoded string
|
|
|
|
:param object solver_problems: result of :class:`Pool::Solver::solve()`
|
|
|
|
:return: solver problem info and solutions
|
|
|
|
:rtype: str
|
|
"""
|
|
if solver_problems:
|
|
problems = ElementTree.Element('problems')
|
|
for problem in solver_problems:
|
|
problem_detail = ElementTree.SubElement(
|
|
problems, 'problem', id=format(problem.id),
|
|
message=problem.findproblemrule().info().problemstr()
|
|
)
|
|
for solution in problem.solutions():
|
|
solution_detail = ElementTree.SubElement(
|
|
problem_detail, 'solution', id=format(solution.id)
|
|
)
|
|
for option in solution.elements(1):
|
|
solution_option = ElementTree.SubElement(
|
|
solution_detail, 'option'
|
|
)
|
|
solution_option.text = option.str()
|
|
|
|
xml_data_unformatted = ElementTree.tostring(
|
|
problems, 'utf-8'
|
|
)
|
|
xml_data_domtree = minidom.parseString(xml_data_unformatted)
|
|
return xml_data_domtree.toprettyxml(indent=" ")
|
|
|
|
def _evaluate_solver_result(self, solver_transaction):
|
|
"""
|
|
Iterate over solver result and return a data dictionary
|
|
|
|
:param object solver_transaction:
|
|
result of :class:`Pool::Solver::transaction()`
|
|
|
|
:return: dict of packages and their details
|
|
|
|
:rtype: dict
|
|
"""
|
|
result_type = namedtuple(
|
|
'result_type', [
|
|
'uri', 'installsize_bytes', 'arch', 'version', 'checksum'
|
|
]
|
|
)
|
|
result = {}
|
|
for solvable in solver_transaction.newpackages():
|
|
name = solvable.lookup_str(self.solv.SOLVABLE_NAME)
|
|
result[name] = result_type(
|
|
uri=solvable.repo.name,
|
|
installsize_bytes=solvable.lookup_num(
|
|
self.solv.SOLVABLE_INSTALLSIZE
|
|
),
|
|
arch=solvable.lookup_str(
|
|
self.solv.SOLVABLE_ARCH
|
|
),
|
|
version=solvable.lookup_str(
|
|
self.solv.SOLVABLE_EVR
|
|
),
|
|
checksum=solvable.lookup_checksum(
|
|
self.solv.SOLVABLE_CHECKSUM
|
|
)
|
|
)
|
|
return result
|
|
|
|
def _setup_jobs(self, job_names, skip_missing):
|
|
"""
|
|
Create a solver job list from given list of job names
|
|
|
|
:param list job_names: list of package,pattern,group names
|
|
:param bool skip_missing: continue or raise if job selection failed
|
|
|
|
:return: list of :class:`Pool.selection()` objects
|
|
|
|
:rtype: list
|
|
"""
|
|
jobs = []
|
|
for job_name in job_names:
|
|
selection_name = self.solv.Selection.SELECTION_NAME
|
|
selection_provides = self.solv.Selection.SELECTION_PROVIDES
|
|
selection = self.pool.select(
|
|
job_name, selection_name | selection_provides
|
|
)
|
|
if selection.flags & self.solv.Selection.SELECTION_PROVIDES:
|
|
log.info('--> Using capability match for {0}'.format(job_name))
|
|
if selection.isempty():
|
|
if skip_missing:
|
|
log.info(
|
|
'--> Package {0} not found: skipped'.format(job_name)
|
|
)
|
|
else:
|
|
raise KiwiSatSolverJobError(
|
|
'Package {0} not found'.format(job_name)
|
|
)
|
|
else:
|
|
jobs += selection.jobs(self.solv.Job.SOLVER_INSTALL)
|
|
|
|
return jobs
|