|
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: |
@@ -210,14 +210,14 @@ class VisibilityCheck(feeds.EvaluateDepSet, feeds.QueryCache, Check): |
210 | 210 |
|
211 | 211 | required_addons = (addons.profiles.ProfileAddon,) |
212 | 212 | known_results = frozenset( |
213 | | - [ |
| 213 | + { |
214 | 214 | VisibleVcsPkg, |
215 | 215 | NonexistentDeps, |
216 | 216 | UncheckableDep, |
217 | 217 | NonsolvableDepsInStable, |
218 | 218 | NonsolvableDepsInDev, |
219 | 219 | NonsolvableDepsInExp, |
220 | | - ] |
| 220 | + } |
221 | 221 | ) |
222 | 222 |
|
223 | 223 | def __init__(self, *args, profile_addon): |
@@ -386,3 +386,71 @@ def process_depset(self, pkg, attr, depset, edepset, profiles): |
386 | 386 | failures.update(required) |
387 | 387 | if failures: |
388 | 388 | yield profile, failures |
| 389 | + |
| 390 | + |
| 391 | +class RdependCycle(results.VersionResult, results.Warning): |
| 392 | + def __init__(self, cycle, **kwargs): |
| 393 | + super().__init__(**kwargs) |
| 394 | + self.cycle = cycle |
| 395 | + |
| 396 | + @property |
| 397 | + def desc(self): |
| 398 | + return f"cycle detected: {' -> '.join(self.cycle)}" |
| 399 | + |
| 400 | + |
| 401 | +class RdependCycleCheck(RepoCheck, OptionalCheck): |
| 402 | + _source = sources.PackageRepoSource |
| 403 | + known_results = frozenset({RdependCycle}) |
| 404 | + |
| 405 | + def __init__(self, options, **kwargs): |
| 406 | + super().__init__(options, **kwargs) |
| 407 | + self.visited_packages: dict[str, frozenset[str]] = {} |
| 408 | + self.repo = self.options.target_repo |
| 409 | + self.no_cycle = set() |
| 410 | + |
| 411 | + def _verify_dfs(self, key: str, path: list[str], visited: set[str]): |
| 412 | + if key in path: |
| 413 | + path.append(key) |
| 414 | + return path |
| 415 | + assert key in self.visited_packages |
| 416 | + |
| 417 | + visited.add(key) |
| 418 | + path.append(key) |
| 419 | + for dep in self.visited_packages[key] - self.no_cycle: |
| 420 | + if cycle := self._verify_dfs(dep, path, visited): |
| 421 | + return cycle |
| 422 | + path.pop() |
| 423 | + self.no_cycle.add(key) |
| 424 | + return [] |
| 425 | + |
| 426 | + def _collect_deps_graph(self, pkgset): |
| 427 | + key = pkgset[0].key |
| 428 | + |
| 429 | + if key in self.visited_packages: |
| 430 | + return |
| 431 | + |
| 432 | + pkg_deps = { |
| 433 | + pkg: {dep.key for dep in pkg.rdepend if isinstance(dep, atom) and not dep.blocks} |
| 434 | + for pkg in pkgset |
| 435 | + } |
| 436 | + self.visited_packages[key] = all_deps = frozenset().union(*pkg_deps.values()) |
| 437 | + if missing := all_deps - self.visited_packages.keys(): |
| 438 | + for missing_key in missing: |
| 439 | + self._collect_deps_graph(self.repo.match(atom(missing_key))) |
| 440 | + return pkg_deps |
| 441 | + |
| 442 | + def feed(self, pkgset): |
| 443 | + key = pkgset[0].key |
| 444 | + |
| 445 | + if key in self.visited_packages: |
| 446 | + pkg_deps = { |
| 447 | + pkg: {dep.key for dep in pkg.rdepend if isinstance(dep, atom) and not dep.blocks} |
| 448 | + for pkg in pkgset |
| 449 | + } |
| 450 | + else: |
| 451 | + pkg_deps = self._collect_deps_graph(pkgset) |
| 452 | + |
| 453 | + for pkg in pkgset: |
| 454 | + for dep in pkg_deps[pkg]: |
| 455 | + if (cycle := self._verify_dfs(dep, [key], set())) and cycle[-1] == key: |
| 456 | + yield RdependCycle(cycle, pkg=pkg) |
0 commit comments