Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 86 additions & 2 deletions src/pkgcheck/checks/visibility.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from collections import defaultdict
from itertools import groupby
from operator import attrgetter

from pkgcore.ebuild.atom import atom, transitive_use_atom
Expand All @@ -7,8 +8,8 @@
from snakeoil.sequences import iflatten_func, iflatten_instance, stable_unique
from snakeoil.strings import pluralism

from .. import addons, feeds, results
from . import Check
from .. import addons, feeds, results, sources
from . import Check, OptionalCheck, RepoCheck


class FakeConfigurable:
Expand Down Expand Up @@ -465,3 +466,86 @@ def process_depset(self, pkg, attr, depset, edepset, profiles):
failures.update(required)
if failures:
yield profile, failures


class RdependCycle(results.VersionResult, results.Warning):
def __init__(self, cycle, **kwargs):
super().__init__(**kwargs)
self.cycle = cycle

@property
def desc(self):
return f"cycle detected: {' -> '.join(self.cycle)}"


class RdependCycleCheck(RepoCheck, OptionalCheck):
_source = sources.PackageRepoSource
known_results = frozenset({RdependCycle})

def __init__(self, options, **kwargs):
super().__init__(options, **kwargs)
self.visited_packages: dict[str, frozenset[str]] = {}
self.repo = self.options.target_repo
self.no_cycle = set()

def find_cycle(self, start_key: str):
path: list[str] = []
visited: set[str] = set()

def dfs(node):
visited.add(node)
path.append(node)

for neighbor in sorted(self.visited_packages[node] - self.no_cycle):
if neighbor not in visited:
if result := dfs(neighbor):
return result
elif neighbor in path and neighbor == start_key:
# Found a cycle that ends at the start node
idx = path.index(start_key)
return path[idx:] + [start_key]

path.pop()
self.no_cycle.add(node)
return None

return dfs(start_key)

def _collect_deps_graph(self, key: str, pkgset):
pkg_deps = {
pkg: {
f"{dep.key}:{dep.slot}" if dep.slot is not None else dep.key
for dep in pkg.rdepend
if isinstance(dep, atom) and not dep.blocks
}
for pkg in pkgset
}

if key in self.visited_packages:
return pkg_deps

self.visited_packages[key] = all_deps = frozenset().union(*pkg_deps.values())
if missing := all_deps - self.visited_packages.keys():
for missing_key in missing:
try:
self._collect_deps_graph(missing_key, self.repo.match(atom(missing_key)))
except IndexError: # NonexistentDeps, invalid dep
self.visited_packages[missing_key] = frozenset()
return pkg_deps

def _collect_graph_variants(self, pkgset):
self._collect_deps_graph(key := pkgset[0].key, pkgset)
yield key, pkgset

def key_func(x):
return f"{x.key}:{x.slot}"

for key, pkgs in groupby(sorted(pkgset, key=key_func), key_func):
self._collect_deps_graph(key, pkgs)
yield key, pkgs

def feed(self, pkgset):
for key, pkg_deps in self._collect_graph_variants(pkgset):
if (cycle := self.find_cycle(key)) and cycle[-1] == key:
for pkg in pkg_deps:
yield RdependCycle(cycle, pkg=pkg)
Loading