Skip to content

Commit fd8e6d7

Browse files
committed
VisibilityCheck: walk the dep tree instead of its CNF form
process_depset() enumerated edepset.iter_cnf_solutions() and then walked the resulting clause list once per profile. pkgcore's OrRestriction converts to CNF by distributing, so an any-of block of N all-of branches of M atoms each yields M^N clauses. That shape is exactly what python_gen_any_dep emits, one branch per implementation, so dev-python/click with distutils_enable_sphinx (6 impls, 7 atoms per branch) produced 117,649 clauses, and every further implementation multiplied the runtime by 7: impls clauses enumeration 6 117,649 0.023s 7 823,543 0.294s 8 5,764,801 3.040s 9 40,353,607 22.294s Hence the CI timeout on the reverted commit adding python3_15, and the distutils_enable_sphinx calls commented out in click and cython. Evaluate the node tree directly instead: an atom is solvable per the existing per-profile caches, an all-of block needs every child, an any-of block needs one. This is equivalent to requiring every CNF clause to hold, blockers included -- a clause containing a blocker was dropped, which matches treating a blocker as solvable -- but linear in the size of the depset rather than exponential. The only divergence is that the CNF form also collected atoms reached solely through redundant superset clauses, so the reported solutions list is now tighter; that case doesn't occur anywhere in ::gentoo. While here, honor the insoluble cache. "if node in insoluble: pass" was a no-op, so a known-unsolvable atom was re-queried on every clause it appeared in. Scanning the two packages from the report with their sphinx deps restored, -j10: click 21.2s real / 56.0s CPU -> 5.9s / 5.1s, cython 5.9s / 14.8s -> 2.9s / 3.7s. Resolves: #782 Signed-off-by: Arthur Zamarin <arthurzam@gentoo.org>
1 parent ce7263a commit fd8e6d7

9 files changed

Lines changed: 94 additions & 42 deletions

File tree

‎NEWS.rst‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ pkgcheck 0.10.42 (unreleased)
3131
- RepoProfilesCheck: don't flag a directory-form ``package.use`` and similar as
3232
an unused profile directory (Arthur Zamarin, #560)
3333

34+
- VisibilityCheck: walk the dep tree instead of enumerating its CNF form, which
35+
was exponential in the number of all-of blocks nested inside an any-of block.
36+
Scanning a package using ``python_gen_any_dep`` with a handful of
37+
implementations, e.g. ``dev-python/click`` with ``distutils_enable_sphinx``,
38+
drops from 21s to 4s, and each further implementation no longer multiplies the
39+
runtime by the size of the any-of block (Arthur Zamarin, #782)
40+
3441
-----------------------------
3542
pkgcheck 0.10.41 (2026-07-24)
3643
-----------------------------

‎src/pkgcheck/checks/visibility.py‎

Lines changed: 51 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -486,51 +486,60 @@ def check_optfeature(self, pkg):
486486
if not self.query_cache[search]:
487487
yield OptfeatureNonexistentAtom(str(search), line=line, lineno=lineno + 1, pkg=pkg)
488488

489-
def process_depset(self, pkg, attr, depset, edepset, profiles):
490-
get_cached_query = self.query_cache.get
489+
def _solvable(self, profile, node):
490+
# is it visible? ie, is it masked?
491+
# if so, skip it.
492+
# long term, probably should do testing in the same respect we do
493+
# for other visibility tiers
494+
if node in profile.cache or profile.provides_has_match(node):
495+
return True
496+
if node in profile.insoluble:
497+
return False
491498

492-
csolutions = []
493-
for required in edepset.iter_cnf_solutions():
494-
for node in required:
495-
if node.blocks:
496-
break
497-
else:
498-
csolutions.append(required)
499+
# get is required since there is an intermix between old style
500+
# virtuals and new style- thus the cache priming doesn't get
501+
# all of it.
502+
src = self.query_cache.get(node.no_usedeps, ())
503+
if node.use:
504+
src = (FakeConfigurable(pkg, profile) for pkg in src)
505+
src = (pkg for pkg in src if node.force_True(pkg))
506+
if any(map(profile.visible, src)):
507+
profile.cache.add(node)
508+
return True
509+
profile.insoluble.add(node)
510+
return False
511+
512+
def _solve_depset(self, profile, node, failures):
513+
if isinstance(node, atom):
514+
# blockers are handled by other checks
515+
if node.blocks or self._solvable(profile, node):
516+
return True
517+
failures.add(node)
518+
return False
519+
520+
if isinstance(node, boolean.OrRestriction):
521+
if not node.restrictions:
522+
return True
523+
# an any-of block is solvable if any of its children is, so the
524+
# failures of its children only matter if all of them fail
525+
nested = set()
526+
for child in node.restrictions:
527+
if self._solve_depset(profile, child, nested):
528+
return True
529+
failures.update(nested)
530+
return False
531+
532+
# everything else is an all-of block, the depset itself included
533+
solvable = True
534+
for child in node.restrictions:
535+
# every child is walked, so that all failures get collected
536+
if not self._solve_depset(profile, child, failures):
537+
solvable = False
538+
return solvable
499539

540+
def process_depset(self, pkg, attr, depset, edepset, profiles):
500541
for profile in profiles:
501542
failures = set()
502-
# is it visible? ie, is it masked?
503-
# if so, skip it.
504-
# long term, probably should do testing in the same respect we do
505-
# for other visibility tiers
506-
cache = profile.cache
507-
provided = profile.provides_has_match
508-
insoluble = profile.insoluble
509-
visible = profile.visible
510-
for required in csolutions:
511-
# scan all of the quickies, the caches...
512-
for node in required:
513-
if node in cache or provided(node):
514-
break
515-
else:
516-
for node in required:
517-
if node in insoluble:
518-
pass
519-
520-
# get is required since there is an intermix between old style
521-
# virtuals and new style- thus the cache priming doesn't get
522-
# all of it.
523-
src = get_cached_query(node.no_usedeps, ())
524-
if node.use:
525-
src = (FakeConfigurable(pkg, profile) for pkg in src)
526-
src = (pkg for pkg in src if node.force_True(pkg))
527-
if any(visible(pkg) for pkg in src):
528-
cache.add(node)
529-
break
530-
else:
531-
insoluble.add(node)
532-
else:
533-
# no matches. not great, should collect them all
534-
failures.update(required)
543+
self._solve_depset(profile, edepset, failures)
535544
if failures:
536545
yield profile, failures
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
{"__class__": "NonsolvableDepsInStable", "category": "NonsolvableDepsInStable", "package": "masked", "version": "0", "attr": "depend", "keyword": "amd64", "profile": "visibility/amd64/stable", "deps": ["stub/masked"], "profile_status": "stable", "profile_deprecated": false, "num_profiles": null}
22
{"__class__": "NonsolvableDepsInStable", "category": "NonsolvableDepsInStable", "package": "masked", "version": "0", "attr": "depend", "keyword": "~amd64", "profile": "visibility/amd64/stable", "deps": ["stub/masked"], "profile_status": "stable", "profile_deprecated": false, "num_profiles": null}
33
{"__class__": "NonsolvableDepsInStable", "category": "NonsolvableDepsInStable", "package": "unstable", "version": "0", "attr": "depend", "keyword": "amd64", "profile": "visibility/amd64/stable", "deps": ["stub/unstable"], "profile_status": "stable", "profile_deprecated": false, "num_profiles": null}
4+
{"__class__": "NonsolvableDepsInStable", "category": "NonsolvableDepsInStable", "package": "any-of", "version": "0", "attr": "depend", "keyword": "amd64", "profile": "visibility/amd64/stable", "deps": ["stub/masked", "stub/unstable"], "profile_status": "stable", "profile_deprecated": false, "num_profiles": null}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
{"__class__": "NonsolvableDepsInStable", "category": "NonsolvableDepsInStable", "package": "masked", "version": "0", "attr": "depend", "keyword": "amd64", "profile": "visibility/amd64/stable", "deps": ["stub/masked"], "profile_status": "stable", "profile_deprecated": false, "num_profiles": 2}
22
{"__class__": "NonsolvableDepsInStable", "category": "NonsolvableDepsInStable", "package": "unstable", "version": "0", "attr": "depend", "keyword": "amd64", "profile": "visibility/amd64/stable", "deps": ["stub/unstable"], "profile_status": "stable", "profile_deprecated": false, "num_profiles": 1}
3+
{"__class__": "NonsolvableDepsInStable", "category": "NonsolvableDepsInStable", "package": "any-of", "version": "0", "attr": "depend", "keyword": "amd64", "profile": "visibility/amd64/stable", "deps": ["stub/masked", "stub/unstable"], "profile_status": "stable", "profile_deprecated": false, "num_profiles": 1}

‎testdata/data/repos/visibility/VisibilityCheck/NonsolvableDepsInStable/fix.patch‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,13 @@ diff -Naur visibility/NonsolvableDepsInStable/unstable/unstable-0.ebuild fixed/N
1616
KEYWORDS="amd64"
1717
-DEPEND="stub/unstable"
1818
+DEPEND="stub/stable"
19+
diff -Naur visibility/NonsolvableDepsInStable/any-of/any-of-0.ebuild fixed/NonsolvableDepsInStable/any-of/any-of-0.ebuild
20+
--- visibility/NonsolvableDepsInStable/any-of/any-of-0.ebuild 2026-07-31 22:22:33.351689541 +0300
21+
+++ fixed/NonsolvableDepsInStable/any-of/any-of-0.ebuild 2026-07-31 22:22:33.355676005 +0300
22+
@@ -7,5 +7,5 @@
23+
DEPEND="
24+
|| (
25+
( stub/stable stub/unstable )
26+
- ( stub/stable stub/masked )
27+
+ ( stub/stable stub/deprecated )
28+
)"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
EAPI=7
2+
DESCRIPTION="Ebuild with an any-of dep where one all-of branch is solvable"
3+
HOMEPAGE="https://github.com/pkgcore/pkgcheck"
4+
LICENSE="BSD"
5+
SLOT="0"
6+
KEYWORDS="amd64"
7+
DEPEND="
8+
|| (
9+
( stub/stable stub/masked )
10+
( stub/stable stub/deprecated )
11+
)"
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
EAPI=7
2+
DESCRIPTION="Ebuild with an any-of dep where every all-of branch is nonsolvable"
3+
HOMEPAGE="https://github.com/pkgcore/pkgcheck"
4+
LICENSE="BSD"
5+
SLOT="0"
6+
KEYWORDS="amd64"
7+
DEPEND="
8+
|| (
9+
( stub/stable stub/unstable )
10+
( stub/stable stub/masked )
11+
)"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
NonsolvableDepsInExp/masked
2+
NonsolvableDepsInStable/any-of
23
NonsolvableDepsInStable/masked
34
NonsolvableDepsInStable/unstable
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
NonsolvableDepsInDev/masked
2+
NonsolvableDepsInStable/any-of
23
NonsolvableDepsInStable/masked
34
NonsolvableDepsInStable/unstable

0 commit comments

Comments
 (0)