/pym/_emerge/resolver/backtracking.py

https://github.com/1000timesdead/portage-funtoo
Python | 197 lines | 165 code | 20 blank | 12 comment | 9 complexity | 23bb9f3f450dac3c882e5abd74b536c2 MD5 | raw file
  1. # Copyright 2010-2011 Gentoo Foundation
  2. # Distributed under the terms of the GNU General Public License v2
  3. import copy
  4. class BacktrackParameter(object):
  5. __slots__ = (
  6. "needed_unstable_keywords", "runtime_pkg_mask", "needed_use_config_changes", "needed_license_changes",
  7. "rebuild_list", "reinstall_list", "needed_p_mask_changes"
  8. )
  9. def __init__(self):
  10. self.needed_unstable_keywords = set()
  11. self.needed_p_mask_changes = set()
  12. self.runtime_pkg_mask = {}
  13. self.needed_use_config_changes = {}
  14. self.needed_license_changes = {}
  15. self.rebuild_list = set()
  16. self.reinstall_list = set()
  17. def __deepcopy__(self, memo=None):
  18. if memo is None:
  19. memo = {}
  20. result = BacktrackParameter()
  21. memo[id(self)] = result
  22. #Shallow copies are enough here, as we only need to ensure that nobody adds stuff
  23. #to our sets and dicts. The existing content is immutable.
  24. result.needed_unstable_keywords = copy.copy(self.needed_unstable_keywords)
  25. result.needed_p_mask_changes = copy.copy(self.needed_p_mask_changes)
  26. result.runtime_pkg_mask = copy.copy(self.runtime_pkg_mask)
  27. result.needed_use_config_changes = copy.copy(self.needed_use_config_changes)
  28. result.needed_license_changes = copy.copy(self.needed_license_changes)
  29. result.rebuild_list = copy.copy(self.rebuild_list)
  30. result.reinstall_list = copy.copy(self.reinstall_list)
  31. return result
  32. def __eq__(self, other):
  33. return self.needed_unstable_keywords == other.needed_unstable_keywords and \
  34. self.needed_p_mask_changes == other.needed_p_mask_changes and \
  35. self.runtime_pkg_mask == other.runtime_pkg_mask and \
  36. self.needed_use_config_changes == other.needed_use_config_changes and \
  37. self.needed_license_changes == other.needed_license_changes and \
  38. self.rebuild_list == other.rebuild_list and \
  39. self.reinstall_list == other.reinstall_list
  40. class _BacktrackNode:
  41. __slots__ = (
  42. "parameter", "depth", "mask_steps", "terminal",
  43. )
  44. def __init__(self, parameter=BacktrackParameter(), depth=0, mask_steps=0, terminal=True):
  45. self.parameter = parameter
  46. self.depth = depth
  47. self.mask_steps = mask_steps
  48. self.terminal = terminal
  49. def __eq__(self, other):
  50. return self.parameter == other.parameter
  51. class Backtracker(object):
  52. __slots__ = (
  53. "_max_depth", "_unexplored_nodes", "_current_node", "_nodes", "_root",
  54. )
  55. def __init__(self, max_depth):
  56. self._max_depth = max_depth
  57. self._unexplored_nodes = []
  58. self._current_node = None
  59. self._nodes = []
  60. self._root = _BacktrackNode()
  61. self._add(self._root)
  62. def _add(self, node, explore=True):
  63. """
  64. Adds a newly computed backtrack parameter. Makes sure that it doesn't already exist and
  65. that we don't backtrack deeper than we are allowed by --backtrack.
  66. """
  67. if node.mask_steps <= self._max_depth and node not in self._nodes:
  68. if explore:
  69. self._unexplored_nodes.append(node)
  70. self._nodes.append(node)
  71. def get(self):
  72. """
  73. Returns a backtrack parameter. The backtrack graph is explored with depth first.
  74. """
  75. if self._unexplored_nodes:
  76. node = self._unexplored_nodes.pop()
  77. self._current_node = node
  78. return copy.deepcopy(node.parameter)
  79. else:
  80. return None
  81. def __len__(self):
  82. return len(self._unexplored_nodes)
  83. def _feedback_slot_conflict(self, conflict_data):
  84. for pkg, parent_atoms in conflict_data:
  85. new_node = copy.deepcopy(self._current_node)
  86. new_node.depth += 1
  87. new_node.mask_steps += 1
  88. new_node.terminal = False
  89. new_node.parameter.runtime_pkg_mask.setdefault(
  90. pkg, {})["slot conflict"] = parent_atoms
  91. self._add(new_node)
  92. def _feedback_missing_dep(self, dep):
  93. new_node = copy.deepcopy(self._current_node)
  94. new_node.depth += 1
  95. new_node.mask_steps += 1
  96. new_node.terminal = False
  97. new_node.parameter.runtime_pkg_mask.setdefault(
  98. dep.parent, {})["missing dependency"] = \
  99. set([(dep.parent, dep.root, dep.atom)])
  100. self._add(new_node)
  101. def _feedback_config(self, changes, explore=True):
  102. """
  103. Handle config changes. Don't count config changes for the maximum backtrack depth.
  104. """
  105. new_node = copy.deepcopy(self._current_node)
  106. new_node.depth += 1
  107. para = new_node.parameter
  108. for change, data in changes.items():
  109. if change == "needed_unstable_keywords":
  110. para.needed_unstable_keywords.update(data)
  111. elif change == "needed_p_mask_changes":
  112. para.needed_p_mask_changes.update(data)
  113. elif change == "needed_license_changes":
  114. for pkg, missing_licenses in data:
  115. para.needed_license_changes.setdefault(pkg, set()).update(missing_licenses)
  116. elif change == "needed_use_config_changes":
  117. for pkg, (new_use, new_changes) in data:
  118. para.needed_use_config_changes[pkg] = (new_use, new_changes)
  119. elif change == "rebuild_list":
  120. para.rebuild_list.update(data)
  121. elif change == "reinstall_list":
  122. para.reinstall_list.update(data)
  123. self._add(new_node, explore=explore)
  124. self._current_node = new_node
  125. def feedback(self, infos):
  126. """
  127. Takes information from the depgraph and computes new backtrack parameters to try.
  128. """
  129. assert self._current_node is not None, "call feedback() only after get() was called"
  130. #Not all config changes require a restart, that's why they can appear together
  131. #with other conflicts.
  132. if "config" in infos:
  133. self._feedback_config(infos["config"], explore=(len(infos)==1))
  134. #There is at most one of the following types of conflicts for a given restart.
  135. if "slot conflict" in infos:
  136. self._feedback_slot_conflict(infos["slot conflict"])
  137. elif "missing dependency" in infos:
  138. self._feedback_missing_dep(infos["missing dependency"])
  139. def backtracked(self):
  140. """
  141. If we didn't backtrack, there is only the root.
  142. """
  143. return len(self._nodes) > 1
  144. def get_best_run(self):
  145. """
  146. Like, get() but returns the backtrack parameter that has as many config changes as possible,
  147. but has no masks. This makes --autounmask effective, but prevents confusing error messages
  148. with "masked by backtracking".
  149. """
  150. best_node = self._root
  151. for node in self._nodes:
  152. if node.terminal and node.depth > best_node.depth:
  153. best_node = node
  154. return copy.deepcopy(best_node.parameter)