commit: b9cedd4a7b0616c0231196b5ee867a6af909d654
Author: Arthur Zamarin <arthurzam <AT> gentoo <DOT> org>
AuthorDate: Thu Dec 15 19:04:46 2022 +0000
Commit: Arthur Zamarin <arthurzam <AT> gentoo <DOT> org>
CommitDate: Tue Dec 20 19:33:22 2022 +0000
URL:
https://gitweb.gentoo.org/proj/pkgcore/pkgcore.git/commit/?id=b9cedd4a
Add REQUIRED_USE satisfaction solver
Based on new `snakeoil.constraints`, this adds a solver which can build
satisfying USE flag combinations for a given set of REQUIRED_USE
restricts. This will be used by new tool `pkgdev tatt`.
Signed-off-by: Arthur Zamarin <arthurzam <AT> gentoo.org>
pyproject.toml | 4 +-
src/pkgcore/restrictions/required_use.py | 118 ++++++++++++++++++++++++++++++
tests/restrictions/test_required_use.py | 121 +++++++++++++++++++++++++++++++
3 files changed, 241 insertions(+), 2 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 16c48bd22..a3d891463 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,7 +1,7 @@
[build-system]
requires = [
"flit_core >=3.8,<4",
- "snakeoil~=0.10.3",
+ "snakeoil~=0.10.4",
]
build-backend = "py_build"
backend-path = ["."]
@@ -28,7 +28,7 @@ classifiers = [
dynamic = ["version"]
dependencies = [
- "snakeoil~=0.10.3",
+ "snakeoil~=0.10.4",
"lxml",
]
diff --git a/src/pkgcore/restrictions/required_use.py
b/src/pkgcore/restrictions/required_use.py
new file mode 100644
index 000000000..a1906d4b0
--- /dev/null
+++ b/src/pkgcore/restrictions/required_use.py
@@ -0,0 +1,118 @@
+from typing import Iterator, Protocol
+
+from snakeoil.constraints import Constraint, Problem
+from . import restriction, boolean, packages, values
+
+
+class _use_constraint(Protocol):
+ def __call__(self, on: frozenset[str]) -> bool:
+ raise NotImplementedError('Constraint', '__call__')
+
+
+def __use_flags_state_any(negate: bool, vals: frozenset[str]) ->
_use_constraint:
+ def check(on: frozenset[str]):
+ return vals.isdisjoint(on) == negate
+ return check
+
+
+def __condition(negate: bool, vals: frozenset[str], *children:
_use_constraint) -> _use_constraint:
+ def check(on: frozenset[str]):
+ return vals.issubset(on) == negate or all(c(on) for c in children)
+ return check
+
+
+def __or_constraint(negate: bool, *children: _use_constraint) ->
_use_constraint:
+ def check(on: frozenset[str]):
+ return any(c(on) for c in children) != negate
+ return check
+
+
+def __and_constraint(negate: bool, *children: _use_constraint) ->
_use_constraint:
+ def check(on: frozenset[str]):
+ return all(c(on) for c in children) != negate
+ return check
+
+
+def __just_one_constraint(negate: bool, *children: _use_constraint) ->
_use_constraint:
+ def check(on: frozenset[str]):
+ return (1 == sum(c(on) for c in children)) != negate
+ return check
+
+
+def __at_most_one_constraint(negate: bool, *children: _use_constraint) ->
_use_constraint:
+ def check(on: frozenset[str]):
+ return (1 >= sum(c(on) for c in children)) != negate
+ return check
+
+
+def __to_single_constraint(restrict) -> tuple[_use_constraint, frozenset[str]]:
+ if isinstance(restrict, values.ContainmentMatch):
+ assert not restrict.all
+ return __use_flags_state_any(restrict.negate,
frozenset(restrict.vals)), frozenset(restrict.vals)
+ elif isinstance(restrict, packages.Conditional):
+ assert isinstance(x := restrict.restriction, values.ContainmentMatch)
+ children, variables = zip(*(__to_single_constraint(c) for c in
restrict.payload))
+ return __condition(x.negate, frozenset(x.vals), *children),
frozenset(x.vals).union(*variables)
+ elif isinstance(restrict, boolean.OrRestriction):
+ children, variables = zip(*(__to_single_constraint(c) for c in
restrict.restrictions))
+ return __or_constraint(restrict.negate, *children),
frozenset().union(*variables)
+ elif isinstance(restrict, boolean.AndRestriction):
+ children, variables = zip(*(__to_single_constraint(c) for c in
restrict.restrictions))
+ return __and_constraint(restrict.negate, *children),
frozenset().union(*variables)
+ elif isinstance(restrict, boolean.JustOneRestriction):
+ children, variables = zip(*(__to_single_constraint(c) for c in
restrict.restrictions))
+ return __just_one_constraint(restrict.negate, *children),
frozenset().union(*variables)
+ elif isinstance(restrict, boolean.AtMostOneOfRestriction):
+ children, variables = zip(*(__to_single_constraint(c) for c in
restrict.restrictions))
+ return __at_most_one_constraint(restrict.negate, *children),
frozenset().union(*variables)
+ else:
+ raise NotImplementedError('build_constraint', type(restrict))
+
+
+def __to_multiple_constraint(restrict) -> Iterator[tuple[_use_constraint,
frozenset[str]]]:
+ if isinstance(restrict, packages.Conditional):
+ assert isinstance(x := restrict.restriction, values.ContainmentMatch)
+ for rule in restrict.payload:
+ for func, variables in __to_multiple_constraint(rule):
+ yield __condition(x.negate, frozenset(x.vals), func),
frozenset(x.vals).union(variables)
+ elif isinstance(restrict, boolean.AndRestriction):
+ assert not restrict.negate
+ for rule in restrict.restrictions:
+ yield from __to_multiple_constraint(rule)
+ else:
+ yield __to_single_constraint(restrict)
+
+
+def __wrapper(constraint_func: _use_constraint) -> Constraint:
+ def check(**kwargs):
+ return constraint_func(frozenset(k for k, v in kwargs.items() if v))
+ return check
+
+
+def find_constraint_satisfaction(restricts: restriction.base, iuse: set[str],
force_true=(), force_false=(), prefer_true=()) -> Iterator[dict[str, bool]]:
+ """Return iterator for use flags combination satisfying REQUIRED_USE
+
+ :param restricts: Parsed restricts of REQUIRED_USE
+ :param iuse: Known IUSE for the restricts. Any USE flag encountered
+ not in this set, will be forced to a False value.
+ :param force_true: USE flags which will be force to only True value.
+ :param force_false: USE flags which will be force to only False value.
+ :param prefer_true: USE flags which will have a preference to True value.
+ All other flags, which aren't forced, will have a preference to False.
+ :return: Iterator returning satisfying use flags combination, of USE flag
+ and it's state.
+ """
+ problem = Problem()
+
+ prefer_false = iuse.difference(force_true, force_false, prefer_true)
+ problem.add_variable((True, False), *prefer_false)
+ problem.add_variable((False, True),
*iuse.intersection(prefer_true).difference(force_false, force_true))
+ problem.add_variable((False, ), *iuse.intersection(force_false))
+ problem.add_variable((True, ), *iuse.intersection(force_true))
+
+ for rule in restricts:
+ for constraint_func, variables in __to_multiple_constraint(rule):
+ if missing_vars := variables - problem.variables.keys():
+ problem.add_variable((False, ), *missing_vars)
+ problem.add_constraint(__wrapper(constraint_func), variables)
+ return iter(problem)
diff --git a/tests/restrictions/test_required_use.py
b/tests/restrictions/test_required_use.py
new file mode 100644
index 000000000..a669c5038
--- /dev/null
+++ b/tests/restrictions/test_required_use.py
@@ -0,0 +1,121 @@
+from itertools import islice
+
+import pytest
+
+from pkgcore.ebuild.eapi import get_eapi
+from pkgcore.ebuild.ebuild_src import base as ebuild
+from pkgcore.restrictions.required_use import find_constraint_satisfaction as
solver
+
+def parse(required_use):
+ o = ebuild(None, 'dev-util/diffball-0.1-r1')
+ object.__setattr__(o, 'eapi', get_eapi('8', suppress_unsupported=True))
+ object.__setattr__(o, 'data', {'REQUIRED_USE': required_use})
+ return o.required_use
+
+def test_simple():
+ required_use = parse(required_use='bar foo')
+ assert tuple(solver(required_use, {'bar', 'foo'})) == ({'bar': True,
'foo': True},)
+
+def test_negative_simple():
+ required_use = parse(required_use='!bar foo')
+ assert tuple(solver(required_use, {'bar', 'foo'})) == ({'bar': False,
'foo': True},)
+
+def test_missing_iuse():
+ required_use = parse(required_use='!bar foo? ( bar )')
+ assert tuple(solver(required_use, {'bar'})) == ({'bar': False, 'foo':
False},)
+
[email protected](('required_use', 'exclude'), (
+ ('bar? ( foo )', {'bar': True, 'foo': False}),
+ ('bar? ( !foo )', {'bar': True, 'foo': True}),
+ ('!bar? ( foo )', {'bar': False, 'foo': False}),
+ ('!bar? ( !foo )', {'bar': False, 'foo': True}),
+))
+def test_condition(required_use, exclude):
+ required_use = parse(required_use=required_use)
+ solutions = tuple(solver(required_use, {'bar', 'foo'}))
+ assert len(solutions) == 3
+ assert exclude not in solutions
+
[email protected](('required_use', 'exclude'), (
+ ('?? ( bar foo )', {'bar': True, 'foo': True}),
+ ('?? ( !bar foo )', {'bar': False, 'foo': True}),
+ ('?? ( bar !foo )', {'bar': True, 'foo': False}),
+ ('?? ( !bar !foo )', {'bar': False, 'foo': False}),
+))
+def test_at_most(required_use, exclude):
+ required_use = parse(required_use=required_use)
+ solutions = tuple(solver(required_use, {'bar', 'foo'}))
+ assert len(solutions) == 3
+ assert exclude not in solutions
+
[email protected](('required_use', 'exclude'), (
+ ('|| ( bar foo )', {'bar': False, 'foo': False}),
+ ('|| ( !bar foo )', {'bar': True, 'foo': False}),
+ ('|| ( bar !foo )', {'bar': False, 'foo': True}),
+ ('|| ( !bar !foo )', {'bar': True, 'foo': True}),
+))
+def test_or(required_use, exclude):
+ required_use = parse(required_use=required_use)
+ solutions = tuple(solver(required_use, {'bar', 'foo'}))
+ assert len(solutions) == 3
+ assert exclude not in solutions
+
[email protected](('required_use', 'include'), (
+ ('bar foo', {'bar': True, 'foo': True}),
+ ('!bar foo', {'bar': False, 'foo': True}),
+ ('bar !foo', {'bar': True, 'foo': False}),
+ ('!bar !foo', {'bar': False, 'foo': False}),
+))
+def test_and(required_use, include):
+ required_use = parse(required_use=required_use)
+ solutions = tuple(solver(required_use, {'bar', 'foo'}))
+ assert solutions == (include,)
+
[email protected](('required_use', 'iuse', 'force_true'), (
+ pytest.param(
+ 'test? ( jpeg jpeg2k tiff truetype )',
+ {'examples', 'imagequant', 'jpeg', 'jpeg2k', 'lcms', 'test', 'tiff',
'tk', 'truetype', 'webp', 'xcb', 'zlib'},
+ {'test'},
+ id='pillow'),
+ pytest.param(
+ 'test? ( cuda gpl? ( openssl? ( bindist ) fdk? ( bindist ) ) ) cuda? (
nvenc ) ^^ ( openssl fdk )',
+ {'cuda', 'gpl', 'openssl', 'bindist', 'fdk', 'test', 'nvenc'},
+ {'test', 'fdk'},
+ id='ffmpeg'),
+ pytest.param(
+ '|| ( openssl ( gnutls ssl ) ) ssl? ( ( gnutls openssl ) )',
+ {'openssl', 'gnutls', 'ssl'},
+ {'ssl'},
+ id='weird'),
+ pytest.param(
+ '|| ( ssl ( gnutls? ( openssl ) ) )',
+ {'openssl', 'gnutls', 'ssl'},
+ {'gnutls'},
+ id='weird2'),
+))
+def test_complex_force_true(required_use, iuse, force_true):
+ required_use = parse(required_use=required_use)
+ solution = None
+ for solution in islice(solver(required_use, iuse, force_true=force_true),
20):
+ assert all(solution[flag] for flag in force_true)
+ use_flags = tuple(k for k, v in solution.items() if v)
+ misses = [restrict for restrict in
required_use.evaluate_depset(use_flags) if not restrict.match(use_flags)]
+ assert not misses
+ assert solution is not None
+
[email protected](('required_use', 'iuse', 'force_false'), (
+ pytest.param(
+ '|| ( openssl ( gnutls ssl ) )',
+ {'openssl', 'gnutls', 'ssl'},
+ {'openssl'},
+ id='custom'),
+))
+def test_complex_force_false(required_use, iuse, force_false):
+ required_use = parse(required_use=required_use)
+ solution = None
+ for solution in islice(solver(required_use, iuse,
force_false=force_false), 20):
+ assert all(not solution[flag] for flag in force_false)
+ use_flags = tuple(k for k, v in solution.items() if v)
+ misses = [restrict for restrict in
required_use.evaluate_depset(use_flags) if not restrict.match(use_flags)]
+ assert not misses
+ assert solution is not None