from operator import attrgetter from snakeoil.mappings import defaultdictkey from snakeoil.strings import pluralism from .. import addons, results, sources from . import Check class RedundantVersion(results.VersionResult, results.Info): """Redundant version(s) of a package in a specific slot.""" def __init__(self, slot, later_versions, **kwargs): super().__init__(**kwargs) self.slot = slot self.later_versions = tuple(later_versions) @property def desc(self): s = pluralism(self.later_versions) versions = ", ".join(self.later_versions) return f"slot({self.slot}) keywords are overshadowed by version{s}: {versions}" class RedundantVersionCheck(Check): """Scan for overshadowed package versions. Scan for versions that are likely shadowed by later versions from a keywords standpoint (ignoring live packages that erroneously have keywords). Example: pkga-1 is keyworded amd64, pkga-2 is amd64. pkga-1 can potentially be removed. """ _source = sources.PackageRepoSource required_addons = (addons.profiles.ProfileAddon,) known_results = frozenset([RedundantVersion]) @staticmethod def mangle_argparser(parser): parser.plugin.add_argument( "--stable-only", action="store_true", help="consider redundant versions only within stable", docs=""" If enabled, for each slot, only consider redundant versions with stable keywords. This is useful for cases of cleanup after successful stabilization. """, ) def __init__(self, *args, profile_addon): super().__init__(*args) self.keywords_profiles = { keyword: sorted(profiles, key=attrgetter("name")) for keyword, profiles in profile_addon.items() } def filter_later_profiles_masks(self, visible_cache, pkg, later_versions): # check both stable/unstable profiles for stable KEYWORDS and only # unstable profiles for unstable KEYWORDS keywords = [] for keyword in pkg.sorted_keywords: if keyword[0] != "~": keywords.append("~" + keyword) keywords.append(keyword) # if a profile exists, where the package is visible, but the later aren't # then it isn't redundant visible_profiles = tuple( profile for keyword in keywords for profile in self.keywords_profiles.get(keyword, ()) if visible_cache[(profile, pkg)] ) return tuple( later for later in later_versions if all(visible_cache[(profile, later)] for profile in visible_profiles) ) def feed(self, pkgset): if len(pkgset) == 1: return # algo is roughly thus; spot stable versions, hunt for subset # keyworded pkgs that are less then the max version; # repeats this for every overshadowing detected # finally, does version comparison down slot lines stack = [] bad = [] for pkg in reversed(pkgset): # reduce false positives for idiot keywords/ebuilds if pkg.live: continue curr_set = {x for x in pkg.keywords if not x.startswith("-")} if not curr_set: continue matches = [ ver for ver, keys in stack if ver.slot == pkg.slot and not curr_set.difference(keys) ] # we've done our checks; now we inject unstable for any stable # via this, earlier versions that are unstable only get flagged # as "not needed" since their unstable flag is a subset of the # stable. # also, yes, have to use list comp here- we're adding as we go curr_set.update([f"~{x}" for x in curr_set if not x.startswith("~")]) stack.append((pkg, curr_set)) if matches: bad.append((pkg, matches)) visible_cache = defaultdictkey(lambda profile_pkg: profile_pkg[0].visible(profile_pkg[1])) for pkg, matches in reversed(bad): if self.options.stable_only and all( key.startswith("~") for x in matches for key in x.keywords ): continue if matches := self.filter_later_profiles_masks(visible_cache, pkg, matches): later_versions = (x.fullver for x in sorted(matches)) yield RedundantVersion(pkg.slot, later_versions, pkg=pkg)