When autounmask is enabled, make depgraph retry _select_atoms calls when
necessary to prefer || choices with existing packages. For example,
if package C is masked and package B does not exist, then autounmask
should choose C when given the choice || ( B C ), as shown in the
included unit tests. The unit tests also show that autounmask still
prefers choices containing packages that are not masked (if available).

Bug: https://bugs.gentoo.org/327177
Signed-off-by: Zac Medico <zmed...@gentoo.org>
---
 lib/_emerge/depgraph.py                       | 152 ++++++++++++------
 .../resolver/test_autounmask_or_choices.py    |  71 ++++++++
 2 files changed, 176 insertions(+), 47 deletions(-)
 create mode 100644 lib/portage/tests/resolver/test_autounmask_or_choices.py

diff --git a/lib/_emerge/depgraph.py b/lib/_emerge/depgraph.py
index 32b2d7da3..7a54aeb34 100644
--- a/lib/_emerge/depgraph.py
+++ b/lib/_emerge/depgraph.py
@@ -365,6 +365,14 @@ class _use_changes(tuple):
                return obj
 
 
+_select_atoms_deps = collections.namedtuple('_select_atoms_deps', (
+       'normal_deps',
+       'virt_deps',
+       'ignored_deps',
+       'unsatisfied_deps',
+))
+
+
 class _dynamic_depgraph_config(object):
 
        """
@@ -3479,8 +3487,13 @@ class depgraph(object):
                                dep.child.root, dep.child.slot_atom, 
installed=False)) and \
                        not slot_operator_rebuild
 
-       def _wrapped_add_pkg_dep_string(self, pkg, dep_root, dep_priority,
-               dep_string, allow_unsatisfied):
+       def _select_atoms_deps(self, pkg, dep_root, dep_priority, 
selected_atoms):
+               """
+               Create Dependency instances from a _select_atoms result, and 
create a
+               list of unsatisfied dependencies which is useful for deciding 
when
+               to retry a _select_atoms call with autounmask enabled.
+               """
+
                if isinstance(pkg.depth, int):
                        depth = pkg.depth + 1
                else:
@@ -3489,38 +3502,13 @@ class depgraph(object):
                deep = self._dynamic_config.myparams.get("deep", 0)
                recurse_satisfied = deep is True or depth <= deep
                debug = "--debug" in self._frozen_config.myopts
-               strict = pkg.type_name != "installed"
-
-               if debug:
-                       writemsg_level("\nParent:    %s\n" % (pkg,),
-                               noiselevel=-1, level=logging.DEBUG)
-                       dep_repr = portage.dep.paren_enclose(dep_string,
-                               unevaluated_atom=True, opconvert=True)
-                       writemsg_level("Depstring: %s\n" % (dep_repr,),
-                               noiselevel=-1, level=logging.DEBUG)
-                       writemsg_level("Priority:  %s\n" % (dep_priority,),
-                               noiselevel=-1, level=logging.DEBUG)
-
-               try:
-                       selected_atoms = self._select_atoms(dep_root,
-                               dep_string, myuse=self._pkg_use_enabled(pkg), 
parent=pkg,
-                               strict=strict, priority=dep_priority)
-               except portage.exception.InvalidDependString:
-                       if pkg.installed:
-                               self._dynamic_config._masked_installed.add(pkg)
-                               return 1
-
-                       # should have been masked before it was selected
-                       raise
-
-               if debug:
-                       writemsg_level("Candidates: %s\n" % \
-                               ([str(x) for x in selected_atoms[pkg]],),
-                               noiselevel=-1, level=logging.DEBUG)
-
                root_config = self._frozen_config.roots[dep_root]
                vardb = root_config.trees["vartree"].dbapi
                traversed_virt_pkgs = set()
+               normal_deps = []
+               virt_deps = []
+               ignored_deps = []
+               unsatisfied_deps = []
 
                reinstall_atoms = self._frozen_config.reinstall_atoms
                for atom, child in self._minimize_children(
@@ -3586,7 +3574,7 @@ class depgraph(object):
                                        # mode may select a different child 
later.
                                        ignored = True
                                        dep.child = None
-                                       
self._dynamic_config._ignored_deps.append(dep)
+                                       ignored_deps.append(dep)
 
                        if not ignored:
                                if dep_priority.ignored and \
@@ -3594,11 +3582,11 @@ class depgraph(object):
                                        if is_virt and dep.child is not None:
                                                
traversed_virt_pkgs.add(dep.child)
                                        dep.child = None
-                                       
self._dynamic_config._ignored_deps.append(dep)
+                                       ignored_deps.append(dep)
                                else:
-                                       if not self._add_dep(dep,
-                                               
allow_unsatisfied=allow_unsatisfied):
-                                               return 0
+                                       if dep.child is None and not 
dep.blocker:
+                                               unsatisfied_deps.append(dep)
+                                       normal_deps.append(dep)
                                        if is_virt and dep.child is not None:
                                                
traversed_virt_pkgs.add(dep.child)
 
@@ -3637,8 +3625,7 @@ class depgraph(object):
                                                # none visible, so use highest
                                                virt_dep.priority.satisfied = 
inst_pkgs[0]
 
-                               if not self._add_pkg(virt_pkg, virt_dep):
-                                       return 0
+                               virt_deps.append(virt_dep)
 
                        for atom, child in self._minimize_children(
                                pkg, self._priority(runtime=True), root_config, 
atoms):
@@ -3686,7 +3673,7 @@ class depgraph(object):
                                        if myarg is None:
                                                ignored = True
                                                dep.child = None
-                                               
self._dynamic_config._ignored_deps.append(dep)
+                                               ignored_deps.append(dep)
 
                                if not ignored:
                                        if dep_priority.ignored and \
@@ -3694,14 +3681,82 @@ class depgraph(object):
                                                if is_virt and dep.child is not 
None:
                                                        
traversed_virt_pkgs.add(dep.child)
                                                dep.child = None
-                                               
self._dynamic_config._ignored_deps.append(dep)
+                                               ignored_deps.append(dep)
                                        else:
-                                               if not self._add_dep(dep,
-                                                       
allow_unsatisfied=allow_unsatisfied):
-                                                       return 0
+                                               if dep.child is None and not 
dep.blocker:
+                                                       
unsatisfied_deps.append(dep)
+                                               normal_deps.append(dep)
                                                if is_virt and dep.child is not 
None:
                                                        
traversed_virt_pkgs.add(dep.child)
 
+               return _select_atoms_deps(normal_deps, virt_deps, ignored_deps, 
unsatisfied_deps)
+
+       def _wrapped_add_pkg_dep_string(self, pkg, dep_root, dep_priority,
+               dep_string, allow_unsatisfied):
+
+               debug = "--debug" in self._frozen_config.myopts
+               strict = pkg.type_name != "installed"
+
+               if debug:
+                       writemsg_level("\nParent:    %s\n" % (pkg,),
+                               noiselevel=-1, level=logging.DEBUG)
+                       dep_repr = portage.dep.paren_enclose(dep_string,
+                               unevaluated_atom=True, opconvert=True)
+                       writemsg_level("Depstring: %s\n" % (dep_repr,),
+                               noiselevel=-1, level=logging.DEBUG)
+                       writemsg_level("Priority:  %s\n" % (dep_priority,),
+                               noiselevel=-1, level=logging.DEBUG)
+
+               autounmask_states = [False]
+               if self._dynamic_config._autounmask:
+                       autounmask_states.append(True)
+
+               choices = []
+               for autounmask in autounmask_states:
+                       if autounmask:
+                               # Clear the package selection cache so that 
autounmask
+                               # can make new selections.
+                               
self._dynamic_config._filtered_trees[dep_root]["porttree"].dbapi._clear_cache()
+                       try:
+                               selected_atoms = self._select_atoms(dep_root,
+                                       dep_string, 
myuse=self._pkg_use_enabled(pkg), parent=pkg,
+                                       strict=strict, priority=dep_priority, 
autounmask=autounmask)
+                       except portage.exception.InvalidDependString:
+                               if pkg.installed:
+                                       
self._dynamic_config._masked_installed.add(pkg)
+                                       return 1
+
+                               # should have been masked before it was selected
+                               raise
+
+                       if debug:
+                               writemsg_level("Candidates (autounmask=%s): 
%s\n" % \
+                                       (autounmask, [str(x) for x in 
selected_atoms[pkg]],),
+                                       noiselevel=-1, level=logging.DEBUG)
+
+                       choice = self._select_atoms_deps(pkg, dep_root, 
dep_priority, selected_atoms)
+                       choices.append(choice)
+                       if not choice.unsatisfied_deps:
+                               break
+               else:
+                       # If all choices have unsatisfied deps, fall back to 
default
+                       # autounmask=False behavior.
+                       choice = choices[0]
+
+                       if autounmask:
+                               # An autounmask choice has been rejected, so 
clear its
+                               # package selections from the cache.
+                               
self._dynamic_config._filtered_trees[dep_root]["porttree"].dbapi._clear_cache()
+
+               for dep in choice.normal_deps:
+                       if not self._add_dep(dep,
+                               allow_unsatisfied=allow_unsatisfied):
+                               return 0
+               for virt_dep in choice.virt_deps:
+                       if not self._add_pkg(virt_dep.child, virt_dep):
+                               return 0
+               self._dynamic_config._ignored_deps.extend(choice.ignored_deps)
+
                if debug:
                        writemsg_level("\nExiting... %s\n" % (pkg,),
                                noiselevel=-1, level=logging.DEBUG)
@@ -4699,7 +4754,8 @@ class depgraph(object):
                return self._select_atoms_highest_available(*pargs, **kwargs)
 
        def _select_atoms_highest_available(self, root, depstring,
-               myuse=None, parent=None, strict=True, trees=None, 
priority=None):
+               myuse=None, parent=None, strict=True, trees=None, priority=None,
+               autounmask=False):
                """This will raise InvalidDependString if necessary. If trees is
                None then self._dynamic_config._filtered_trees is used."""
 
@@ -4727,8 +4783,9 @@ class depgraph(object):
                if True:
                        # Temporarily disable autounmask so that || preferences
                        # account for masking and USE settings.
-                       _autounmask_backup = self._dynamic_config._autounmask
-                       self._dynamic_config._autounmask = False
+                       if not autounmask:
+                               _autounmask_backup = 
self._dynamic_config._autounmask
+                               self._dynamic_config._autounmask = False
                        # backup state for restoration, in case of recursive
                        # calls to this method
                        backup_parent = self._select_atoms_parent
@@ -4756,7 +4813,8 @@ class depgraph(object):
                                        myroot=root, trees=trees)
                        finally:
                                # restore state
-                               self._dynamic_config._autounmask = 
_autounmask_backup
+                               if not autounmask:
+                                       self._dynamic_config._autounmask = 
_autounmask_backup
                                self._select_atoms_parent = backup_parent
                                mytrees.pop("pkg_use_enabled", None)
                                mytrees.pop("parent", None)
diff --git a/lib/portage/tests/resolver/test_autounmask_or_choices.py 
b/lib/portage/tests/resolver/test_autounmask_or_choices.py
new file mode 100644
index 000000000..b5f2044e3
--- /dev/null
+++ b/lib/portage/tests/resolver/test_autounmask_or_choices.py
@@ -0,0 +1,71 @@
+# Copyright 2019 Gentoo Authors
+# Distributed under the terms of the GNU General Public License v2
+
+from portage.tests import TestCase
+from portage.tests.resolver.ResolverPlayground import (
+       ResolverPlayground,
+       ResolverPlaygroundTestCase,
+)
+
+class AutounmaskOrChoicesTestCase(TestCase):
+
+       def testAutounmaskOrChoices(self):
+               ebuilds = {
+                       'dev-libs/A-1': {
+                               'EAPI': '7',
+                               'RDEPEND': '|| ( dev-libs/B dev-libs/C )',
+                       },
+                       'dev-libs/C-1': {
+                               'EAPI': '7',
+                               'KEYWORDS': '~x86',
+                       },
+                       'dev-libs/D-1': {
+                               'EAPI': '7',
+                               'RDEPEND': '|| ( dev-libs/E dev-libs/F )',
+                       },
+                       'dev-libs/E-1': {
+                               'EAPI': '7',
+                               'KEYWORDS': '~x86',
+                       },
+                       'dev-libs/F-1': {
+                               'EAPI': '7',
+                               'KEYWORDS': 'x86',
+                       },
+               }
+
+               test_cases = (
+                       # Test bug 327177, where we want to prefer choices with 
masked
+                       # packages over those with nonexisting packages.
+                       ResolverPlaygroundTestCase(
+                               ['dev-libs/A'],
+                               options={"--autounmask": True},
+                               success=False,
+                               mergelist=[
+                                       'dev-libs/C-1',
+                                       'dev-libs/A-1',
+                               ],
+                               unstable_keywords = ('dev-libs/C-1',),
+                       ),
+                       # Test that autounmask prefers choices with packages 
that
+                       # are not masked.
+                       ResolverPlaygroundTestCase(
+                               ['dev-libs/D'],
+                               options={"--autounmask": True},
+                               success=True,
+                               mergelist=[
+                                       'dev-libs/F-1',
+                                       'dev-libs/D-1',
+                               ],
+                       ),
+               )
+
+               playground = ResolverPlayground(ebuilds=ebuilds, debug=False)
+
+               try:
+                       for test_case in test_cases:
+                               playground.run_TestCase(test_case)
+                               self.assertEqual(test_case.test_success, True,
+                                       test_case.fail_msg)
+               finally:
+                       playground.debug = False
+                       playground.cleanup()
-- 
2.21.0


Reply via email to