|
7 | 7 | from snakeoil.sequences import iflatten_func, iflatten_instance, stable_unique |
8 | 8 | from snakeoil.strings import pluralism |
9 | 9 |
|
10 | | -from .. import addons, feeds, results |
11 | | -from . import Check |
| 10 | +from .. import addons, feeds, results, sources |
| 11 | +from . import Check, OptionalCheck, RepoCheck |
12 | 12 |
|
13 | 13 |
|
14 | 14 | class FakeConfigurable: |
@@ -465,3 +465,74 @@ def process_depset(self, pkg, attr, depset, edepset, profiles): |
465 | 465 | failures.update(required) |
466 | 466 | if failures: |
467 | 467 | yield profile, failures |
| 468 | + |
| 469 | + |
| 470 | +class RdependCycle(results.VersionResult, results.Warning): |
| 471 | + def __init__(self, cycle, **kwargs): |
| 472 | + super().__init__(**kwargs) |
| 473 | + self.cycle = cycle |
| 474 | + |
| 475 | + @property |
| 476 | + def desc(self): |
| 477 | + return f"cycle detected: {' -> '.join(self.cycle)}" |
| 478 | + |
| 479 | + |
| 480 | +class RdependCycleCheck(RepoCheck, OptionalCheck): |
| 481 | + _source = sources.PackageRepoSource |
| 482 | + known_results = frozenset({RdependCycle}) |
| 483 | + |
| 484 | + def __init__(self, options, **kwargs): |
| 485 | + super().__init__(options, **kwargs) |
| 486 | + self.visited_packages: dict[str, frozenset[str]] = {} |
| 487 | + self.repo = self.options.target_repo |
| 488 | + self.no_cycle = set() |
| 489 | + |
| 490 | + def _verify_dfs(self, key: str, path: list[str], visited: set[str]): |
| 491 | + if key in path: |
| 492 | + path.append(key) |
| 493 | + return path |
| 494 | + assert key in self.visited_packages |
| 495 | + |
| 496 | + visited.add(key) |
| 497 | + path.append(key) |
| 498 | + for dep in self.visited_packages[key] - self.no_cycle: |
| 499 | + if cycle := self._verify_dfs(dep, path, visited): |
| 500 | + return cycle |
| 501 | + path.pop() |
| 502 | + self.no_cycle.add(key) |
| 503 | + return [] |
| 504 | + |
| 505 | + def _collect_deps_graph(self, pkgset): |
| 506 | + key = pkgset[0].key |
| 507 | + |
| 508 | + if key in self.visited_packages: |
| 509 | + return |
| 510 | + |
| 511 | + pkg_deps = { |
| 512 | + pkg: {dep.key for dep in pkg.rdepend if isinstance(dep, atom) and not dep.blocks} |
| 513 | + for pkg in pkgset |
| 514 | + } |
| 515 | + self.visited_packages[key] = all_deps = frozenset().union(*pkg_deps.values()) |
| 516 | + if missing := all_deps - self.visited_packages.keys(): |
| 517 | + for missing_key in missing: |
| 518 | + try: |
| 519 | + self._collect_deps_graph(self.repo.match(atom(missing_key))) |
| 520 | + except IndexError: |
| 521 | + self.visited_packages[missing_key] = frozenset() |
| 522 | + return pkg_deps |
| 523 | + |
| 524 | + def feed(self, pkgset): |
| 525 | + key = pkgset[0].key |
| 526 | + |
| 527 | + if key in self.visited_packages: |
| 528 | + pkg_deps = { |
| 529 | + pkg: {dep.key for dep in pkg.rdepend if isinstance(dep, atom) and not dep.blocks} |
| 530 | + for pkg in pkgset |
| 531 | + } |
| 532 | + else: |
| 533 | + pkg_deps = self._collect_deps_graph(pkgset) |
| 534 | + |
| 535 | + for pkg in pkgset: |
| 536 | + for dep in pkg_deps[pkg]: |
| 537 | + if (cycle := self._verify_dfs(dep, [key], set())) and cycle[-1] == key: |
| 538 | + yield RdependCycle(cycle, pkg=pkg) |
0 commit comments