kiwi-el8/kiwi/solver/sat.py
Marcus Schäfer 119e96dc8f Added Sat solver class
Added implementation for Solver class based on the SUSE
libsolv C library and the solv python binding
2016-11-21 12:02:01 +01:00

212 lines
7.3 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
from collections import namedtuple
from xml.etree import ElementTree
from xml.dom import minidom
# project
from ..logger import log
from ..exceptions import (
KiwiSatSolverPluginError,
KiwiSatSolverJobError,
KiwiSatSolverJobProblems
)
class Sat(object):
"""
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
"""
try:
self.solv = importlib.import_module('solv')
self.Pool = getattr(
importlib.import_module('solv', 'Pool'), 'Pool'
)
self.Selection = getattr(
importlib.import_module('solv', 'Selection'), 'Selection'
)
except Exception as e:
raise KiwiSatSolverPluginError(
'{0}: {1}'.format(type(e).__name__, format(e))
)
self.pool = self.Pool()
self.pool.setarch()
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 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):
"""
Solve dependencies for the given job list. The list is allowd
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
:rtype: dict
:return: Transaction result information
"""
solver = self.pool.Solver()
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 Pool::Solver::solve()
:rtype: string
:return: solver problem info and solutions
"""
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 Pool::Solver::transaction()
:rtype: dict
:return: dict of packages and their details
"""
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
:rtype: list
:return: list of Pool.selection() objects
"""
jobs = []
for job_name in job_names:
selection = self.pool.select(
job_name, self.Selection.SELECTION_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