#!/usr/bin/python # -*- coding: utf-8 -*- # Copyright (c) 2017 SUSE LLC # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # TODO: implement equivalent of namespace namespace:language(de) @SYSTEM # TODO: solve all devel packages to include from lxml import etree as ET from collections import namedtuple import sys import cmdln import logging import urllib2 import osc.core import glob import solv from pprint import pprint, pformat import os import subprocess import re import yaml import ToolBase # share header cache with repochecker from osclib.memoize import CACHEDIR logger = logging.getLogger() ARCHITECTURES = ('x86_64', 'ppc64le', 's390x', 'aarch64') DEFAULT_REPOS = ("openSUSE:Factory/standard") class Group(object): def __init__(self, name, pkglist): self.name = name self.safe_name = re.sub(r'\W', '_', name.lower()) self.pkglist = pkglist self.architectures = pkglist.architectures self.conditional = None self.packages = dict() self.locked = set() self.solved_packages = None self.solved = False self.not_found = dict() self.unresolvable = dict() for a in self.architectures: self.packages[a] = [] self.unresolvable[a] = dict() self.srcpkgs = None self.develpkgs = [] self.silents = set() pkglist.groups[self.safe_name] = self def _add_to_packages(self, package, arch=None): archs = self.architectures if arch: archs = [arch] for a in archs: self.packages[a].append([package, self.name]) def parse_yml(self, packages): # package less group is a rare exception if packages is None: return for package in packages: if not isinstance(package, dict): self._add_to_packages(package) continue name = package.keys()[0] for rel in package[name]: if rel == 'locked': self.locked.add(name) elif rel == 'silent': self._add_to_packages(name) self.silents.add(name) else: self._add_to_packages(name, rel) def _verify_solved(self): if not self.solved: raise Exception('group {} not solved'.format(self.name)) def inherit(self, group): for arch in self.architectures: self.packages[arch] += group.packages[arch] self.locked.update(group.locked) self.silents.update(group.silents) # do not repeat packages def ignore(self, without): for arch in ['*'] + self.architectures: s = set(without.solved_packages[arch].keys()) s |= set(without.solved_packages['*'].keys()) for p in s: self.solved_packages[arch].pop(p, None) for p in without.not_found.keys(): if not p in self.not_found: continue self.not_found[p] -= without.not_found[p] if not self.not_found[p]: self.not_found.pop(p) def solve(self, ignore_recommended=False): """ base: list of base groups or None """ if self.solved: return solved = dict() for arch in self.architectures: solved[arch] = dict() self.srcpkgs = set() self.recommends = dict() for arch in self.architectures: pool = self.pkglist._prepare_pool(arch) # pool.set_debuglevel(10) for n, group in self.packages[arch]: jobs = list(self.pkglist.lockjobs[arch]) sel = pool.select(str(n), solv.Selection.SELECTION_NAME) if sel.isempty(): logger.debug('{}.{}: package {} not found'.format(self.name, arch, n)) self.not_found.setdefault(n, set()).add(arch) continue else: jobs += sel.jobs(solv.Job.SOLVER_INSTALL) for l in self.locked: sel = pool.select(str(l), solv.Selection.SELECTION_NAME) if sel.isempty(): logger.warn('{}.{}: locked package {} not found'.format(self.name, arch, l)) else: jobs += sel.jobs(solv.Job.SOLVER_LOCK) for s in self.silents: sel = pool.select(str(s), solv.Selection.SELECTION_NAME | solv.Selection.SELECTION_FLAT) if sel.isempty(): logger.warn('{}.{}: silent package {} not found'.format(self.name, arch, s)) else: jobs += sel.jobs(solv.Job.SOLVER_INSTALL) solver = pool.Solver() if ignore_recommended: solver.set_flag(solver.SOLVER_FLAG_IGNORE_RECOMMENDED, 1) problems = solver.solve(jobs) if problems: for problem in problems: logger.error('unresolvable: %s.%s: %s', self.name, arch, problem) self.unresolvable[arch][n] = str(problem) continue trans = solver.transaction() if trans.isempty(): logger.error('%s.%s: nothing to do', self.name, arch) continue if 'get_recommended' in dir(solver): for s in solver.get_recommended(): self.recommends.setdefault(s.name, group + ':' + n) else: logger.warn('newer libsolv needed for recommends!') for s in trans.newsolvables(): solved[arch].setdefault(s.name, group + ':' + n) reason, rule = solver.describe_decision(s) if None: print(self.name, s.name, reason, rule.info().problemstr()) # don't ask me why, but that's how it seems to work if s.lookup_void(solv.SOLVABLE_SOURCENAME): src = s.name else: src = s.lookup_str(solv.SOLVABLE_SOURCENAME) self.srcpkgs.add(src) common = None # compute common packages across all architectures for arch in self.architectures: if common is None: common = set(solved[arch].keys()) continue common &= set(solved[arch].keys()) if common is None: common = set() # reduce arch specific set by common ones solved['*'] = dict() for arch in self.architectures: for p in common: solved['*'][p] = solved[arch].pop(p) self.solved_packages = solved self.solved = True def collect_devel_packages(self, modules): develpkgs = set() for arch in self.architectures: pool = self.pkglist._prepare_pool(arch) sel = pool.Selection() for s in pool.solvables_iter(): if s.name.endswith('-devel'): # don't ask me why, but that's how it seems to work if s.lookup_void(solv.SOLVABLE_SOURCENAME): src = s.name else: src = s.lookup_str(solv.SOLVABLE_SOURCENAME) if src in self.srcpkgs: develpkgs.add(s.name) self.develpkgs = [] for p in develpkgs: already_present = False for m in modules: for arch in ['*'] + self.architectures: already_present = already_present or (p in m.solved_packages[arch]) already_present = already_present or (p in m.develpkgs) if not already_present: self.develpkgs.append(p) def filter_duplicated_recommends(self, modules): recommends = self.recommends # erase our own - so we don't filter our own self.recommends = dict() for p in recommends: already_present = False for m in modules: for arch in ['*'] + self.architectures: already_present = already_present or (p in m.solved_packages[arch]) already_present = already_present or (p in m.recommends) if not already_present: self.recommends[p] = recommends[p] def toxml(self, arch, ignore_broken = False): packages = self.solved_packages[arch] name = self.name if arch != '*': name += '.' + arch root = ET.Element('group', {'name': name}) c = ET.Comment(' ### AUTOMATICALLY GENERATED, DO NOT EDIT ### ') root.append(c) if arch != '*': cond = ET.SubElement(root, 'conditional', { 'name': 'only_{}'.format(arch)}) packagelist = ET.SubElement( root, 'packagelist', {'relationship': 'recommends'}) missing = dict() if arch == '*': missing = self.not_found unresolvable = self.unresolvable.get(arch, dict()) for name in sorted(packages.keys() + missing.keys() + unresolvable.keys()): if name in self.silents: continue if name in missing: msg = ' {} not found on {}'.format(name, ','.join(sorted(missing[name]))) if ignore_broken: c = ET.Comment(msg) packagelist.append(c) continue name = msg if name in unresolvable: msg = ' {} uninstallable: {}'.format(name, unresolvable[name]) logger.error(msg) if ignore_broken: c = ET.Comment(msg) packagelist.append(c) continue else: name = msg status = self.pkglist.supportstatus(name) attrs = { 'name': name } if status is not None: attrs['supportstatus'] = status p = ET.SubElement(packagelist, 'package', attrs) if name in packages: c = ET.Comment(' reason: {} '.format(packages[name])) packagelist.append(c) if arch == '*' and self.develpkgs: c = ET.Comment("\nDevelopment packages:\n - " + "\n - ".join(sorted(self.develpkgs)) + "\n") root.append(c) if arch == '*' and self.recommends: comment = "\nRecommended packages:\n" for p in sorted(self.recommends.keys()): comment += " - {} # {}\n".format(p, self.recommends[p]) c = ET.Comment(comment) root.append(c) return root def dump(self): pprint({'name': self.name, 'missing': self.missing, 'packages': self.packages, 'solved': self.solved_packages, 'silents': self.silents}) return archs = ['*'] + self.architectures for arch in archs: x = self.toxml(arch) print(ET.tostring(x, pretty_print=True)) class PkgListGen(ToolBase.ToolBase): def __init__(self): ToolBase.ToolBase.__init__(self) self.repos = DEFAULT_REPOS # package -> supportatus self.packages = dict() self.default_support_status = 'l3' self.groups = dict() self._supportstatus = None self.input_dir = '.' self.output_dir = '.' self.lockjobs = dict() self.ignore_broken = False self.ignore_recommended = False def _dump_supportstatus(self): for name in self.packages.keys(): for status in self.packages[name]: if status == self.default_support_status: continue for group in self.packages[name][status]: print name, status def _load_supportstatus(self): # XXX fn = os.path.join(self.input_dir, 'supportstatus.txt') self._supportstatus = dict() if os.path.exists(fn): with open(fn, 'r') as fh: for l in fh: # pkg, status a = l.rstrip().split(' ') if len(a) > 1: self._supportstatus[a[0]] = a[1] # TODO: make per product def supportstatus(self, package): if self._supportstatus is None: self._load_supportstatus() if package in self._supportstatus: return self._supportstatus[package] else: return self.default_support_status def _load_group_file(self, fn): output = None with open(fn, 'r') as fh: logger.debug("reading %s", fn) for groupname, group in yaml.safe_load(fh).items(): if groupname == 'OUTPUT': output = group continue g = Group(groupname, self) g.parse_yml(group) return output def load_all_groups(self): output = None for fn in glob.glob(os.path.join(self.input_dir, 'group*.yml')): o = self._load_group_file(fn) if not output: output = o return output def _write_all_groups(self): self._check_supplements() archs = ['*'] + self.architectures for name in self.groups: group = self.groups[name] fn = '{}.group'.format(group.name) if not group.solved: continue with open(os.path.join(self.output_dir, fn), 'w') as fh: for arch in archs: x = group.toxml(arch, self.ignore_broken) x = ET.tostring(x, pretty_print=True) x = re.sub('\s*