You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
386 lines
15 KiB
386 lines
15 KiB
# -*- coding: utf-8 -*-
|
|
|
|
# Copyright (C) 2012 Niels Thykier <niels@thykier.net>
|
|
# - Includes code by Paul Harrison
|
|
# (http://www.logarithmic.net/pfh-files/blog/01208083168/sort.py)
|
|
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
|
|
import logging
|
|
from collections import deque
|
|
from itertools import chain
|
|
|
|
from britney2.utils import (ifilter_only, iter_except)
|
|
|
|
|
|
class OrderNode(object):
|
|
|
|
__slots__ = ['before', 'after']
|
|
|
|
def __init__(self):
|
|
self.after = set()
|
|
self.before = set()
|
|
|
|
|
|
def compute_scc(graph):
|
|
"""Iterative algorithm for strongly-connected components
|
|
|
|
Iterative variant of Tarjan's algorithm for finding strongly-connected
|
|
components.
|
|
|
|
:param graph: Table of all nodes along which their edges (in "before" and "after")
|
|
:return: List of components (each component is a list of items)
|
|
"""
|
|
result = []
|
|
low = {}
|
|
node_stack = []
|
|
|
|
def _cannot_be_a_scc(graph_node):
|
|
if not graph[graph_node].before or not graph[graph_node].after:
|
|
# Short-cut obviously isolated component
|
|
result.append((graph_node,))
|
|
# Set the item number so high that no other item might
|
|
# mistakenly assume that they can form a component via
|
|
# this item.
|
|
# (Replaces the "is w on the stack check" for us from
|
|
# the original algorithm)
|
|
low[graph_node] = len(graph) + 1
|
|
return True
|
|
return False
|
|
|
|
def _handle_succ(parent, parent_num, successors_remaining):
|
|
while successors_remaining:
|
|
succ = successors_remaining.pop()
|
|
succ_num = low.get(succ, None)
|
|
if succ_num is not None:
|
|
if succ_num < parent_num:
|
|
# These two nodes are part of the probably
|
|
# same SSC (or succ is isolated
|
|
low[parent] = parent_num = succ_num
|
|
continue
|
|
# It cannot be a part of a SCC if it does not have depends
|
|
# or reverse depends.
|
|
if _cannot_be_a_scc(succ):
|
|
continue
|
|
succ_num = len(low)
|
|
low[succ] = succ_num
|
|
work_stack.append((succ, len(node_stack), succ_num, graph[succ].before))
|
|
node_stack.append(succ)
|
|
# "Recurse" into the child node first
|
|
return True
|
|
return False
|
|
|
|
for n in graph:
|
|
if n in low:
|
|
continue
|
|
# It cannot be a part of a SCC if it does not have depends
|
|
# or reverse depends.
|
|
if _cannot_be_a_scc(n):
|
|
continue
|
|
|
|
root_num = len(low)
|
|
low[n] = root_num
|
|
# DFS work-stack needed to avoid call recursion. It (more or less)
|
|
# replaces the variables on the call stack in Tarjan's algorithm
|
|
work_stack = [(n, len(node_stack), root_num, graph[n].before)]
|
|
node_stack.append(n)
|
|
while work_stack:
|
|
node, stack_idx, orig_node_num, successors = work_stack[-1]
|
|
if successors and _handle_succ(node, low[node], successors):
|
|
# _handle_succ has pushed a new node on to work_stack
|
|
# and we need to "restart" the loop to handle that first
|
|
continue
|
|
|
|
# This node is done; remove it from the work stack
|
|
work_stack.pop()
|
|
|
|
# This node is out of successor. Push up the "low" value
|
|
# (Exception: root node has no parent)
|
|
node_num = low[node]
|
|
if work_stack:
|
|
parent = work_stack[-1][0]
|
|
parent_num = low[parent]
|
|
if node_num <= parent_num:
|
|
# This node is a part of a component with its parent.
|
|
# We update the parent's node number and push the
|
|
# responsibility of building the component unto the
|
|
# parent.
|
|
low[parent] = node_num
|
|
continue
|
|
if node_num != orig_node_num:
|
|
# The node is a part of an SCC with a ancestor (and parent)
|
|
continue
|
|
# We got a component
|
|
component = tuple(node_stack[stack_idx:])
|
|
del node_stack[stack_idx:]
|
|
result.append(component)
|
|
# Re-number all items, so no other item might
|
|
# mistakenly assume that they can form a component via
|
|
# one of these items.
|
|
# (Replaces the "is w on the stack check" for us from
|
|
# the original algorithm)
|
|
new_num = len(graph) + 1
|
|
for item in component:
|
|
low[item] = new_num
|
|
|
|
assert not node_stack
|
|
|
|
return result
|
|
|
|
|
|
def apply_order(key, other, order, logger, order_cause, invert=False, order_sub_cause=''):
|
|
if other == key:
|
|
# "Self-relation" => ignore
|
|
return
|
|
order_key = order[key]
|
|
if invert:
|
|
order[other].after.add(key)
|
|
order_set = order_key.before
|
|
else:
|
|
order[other].before.add(key)
|
|
order_set = order_key.after
|
|
if logger.isEnabledFor(logging.DEBUG) and other not in order_set: # pragma: no cover
|
|
if order_sub_cause:
|
|
order_sub_cause = ' (%s)' % order_sub_cause
|
|
logger.debug("%s induced order%s: %s before %s", order_cause, order_sub_cause, key, other)
|
|
# Defer adding until the end to ensure we only log the first time a dependency order is introduced.
|
|
order_set.add(other)
|
|
|
|
|
|
class InstallabilitySolver(object):
|
|
|
|
def __init__(self, universe, inst_tester):
|
|
"""Create a new installability solver
|
|
|
|
universe is a BinaryPackageUniverse.
|
|
"""
|
|
self._universe = universe
|
|
self._inst_tester = inst_tester
|
|
logger_name = ".".join((self.__class__.__module__, self.__class__.__name__))
|
|
self.logger = logging.getLogger(logger_name)
|
|
|
|
def _compute_group_order_rms(self, rms, order, key, ptable, going_out):
|
|
sat_in_testing = self._inst_tester.any_of_these_are_in_the_suite
|
|
universe = self._universe
|
|
logger = self.logger
|
|
for rdep in chain.from_iterable(universe.reverse_dependencies_of(r) for r in rms):
|
|
# The binaries have reverse dependencies in testing;
|
|
# check if we can/should migrate them first.
|
|
for depgroup in universe.dependencies_of(rdep):
|
|
rigid = depgroup - going_out
|
|
if sat_in_testing(rigid):
|
|
# (partly) satisfied by testing, assume it is okay
|
|
continue
|
|
if rdep in ptable:
|
|
apply_order(key, ptable[rdep], order, logger, 'Removal')
|
|
|
|
def _compute_order_for_dependency(self, key, depgroup, ptable, order, going_in):
|
|
# We got three cases:
|
|
# - "swap" (replace existing binary with a newer version)
|
|
# - "addition" (add new binary without removing any)
|
|
# - "removal" (remove binary without providing a new)
|
|
#
|
|
# The problem is that only the two latter requires
|
|
# an ordering. A "swap" (in itself) should not
|
|
# affect us.
|
|
other_adds = set()
|
|
other_rms = set()
|
|
logger = self.logger
|
|
for d in ifilter_only(ptable, depgroup):
|
|
other = ptable[d]
|
|
if d in going_in:
|
|
# "other" provides something "key" needs,
|
|
# schedule accordingly.
|
|
other_adds.add(other)
|
|
else:
|
|
# "other" removes something "key" needs,
|
|
# schedule accordingly.
|
|
other_rms.add(other)
|
|
|
|
for other in other_adds - other_rms:
|
|
apply_order(key, other, order, logger, 'Dependency', order_sub_cause='add')
|
|
for other in other_rms - other_adds:
|
|
apply_order(key, other, order, logger, 'Dependency', order_sub_cause='remove', invert=True)
|
|
|
|
def _compute_group_order_adds(self, adds, order, key, ptable, going_out, going_in):
|
|
sat_in_testing = self._inst_tester.any_of_these_are_in_the_suite
|
|
universe = self._universe
|
|
for depgroup in chain.from_iterable(universe.dependencies_of(a) for a in adds):
|
|
# Check if this item should migrate before others
|
|
# (e.g. because they depend on a new [version of a]
|
|
# binary provided by this item).
|
|
rigid = depgroup - going_out
|
|
if sat_in_testing(rigid):
|
|
# (partly) satisfied by testing, assume it is okay
|
|
continue
|
|
self._compute_order_for_dependency(key, depgroup, ptable, order, going_in)
|
|
|
|
def _compute_group_order(self, groups, key2item):
|
|
universe = self._universe
|
|
ptable = {}
|
|
order = {}
|
|
going_out = set()
|
|
going_in = set()
|
|
logger = self.logger
|
|
debug_solver = logger.isEnabledFor(logging.DEBUG)
|
|
|
|
# Build the tables
|
|
for (item, adds, rms) in groups:
|
|
key = str(item)
|
|
key2item[key] = item
|
|
order[key] = OrderNode()
|
|
going_in.update(adds)
|
|
going_out.update(rms)
|
|
for x in chain(adds, rms):
|
|
ptable[x] = key
|
|
|
|
if debug_solver: # pragma: no cover
|
|
self._dump_groups(groups)
|
|
|
|
# This large loop will add ordering constrains on each "item"
|
|
# that migrates based on various rules.
|
|
for (item, adds, rms) in groups:
|
|
key = str(item)
|
|
oldcons = set(chain.from_iterable(universe.negative_dependencies_of(r) for r in rms))
|
|
newcons = set(chain.from_iterable(universe.negative_dependencies_of(a) for a in adds))
|
|
oldcons -= newcons
|
|
# Some of the old binaries have "conflicts" that will
|
|
# be removed.
|
|
for o in ifilter_only(ptable, oldcons):
|
|
# "key" removes a conflict with one of
|
|
# "other"'s binaries, so it is probably a good
|
|
# idea to migrate "key" before "other"
|
|
apply_order(key, ptable[o], order, logger, 'Conflict', invert=True)
|
|
|
|
self._compute_group_order_rms(rms, order, key, ptable, going_out)
|
|
self._compute_group_order_adds(adds, order, key, ptable, going_out, going_in)
|
|
|
|
return order
|
|
|
|
def _merge_items_into_components(self, comps, order):
|
|
merged = {}
|
|
scc = {}
|
|
debug_solver = self.logger.isEnabledFor(logging.DEBUG)
|
|
for com in comps:
|
|
scc_id = com[0]
|
|
scc[scc_id] = com
|
|
merged[scc_id] = scc_id
|
|
if len(com) < 2:
|
|
# Trivial case
|
|
continue
|
|
so_before = order[scc_id].before
|
|
so_after = order[scc_id].after
|
|
for n in com:
|
|
if n == scc_id:
|
|
continue
|
|
so_before.update(order[n].before)
|
|
so_after.update(order[n].after)
|
|
merged[n] = scc_id
|
|
del order[n]
|
|
if debug_solver: # pragma: no cover
|
|
self.logger.debug("SCC: %s -- %s", scc_id, str(sorted(com)))
|
|
|
|
for com in comps:
|
|
node = com[0]
|
|
nbefore = set(merged[b] for b in order[node].before)
|
|
nafter = set(merged[b] for b in order[node].after)
|
|
|
|
# Drop self-relations (usually caused by the merging)
|
|
nbefore.discard(node)
|
|
nafter.discard(node)
|
|
order[node].before = nbefore
|
|
order[node].after = nafter
|
|
|
|
for com in comps:
|
|
scc_id = com[0]
|
|
|
|
for other_scc_id in order[scc_id].before:
|
|
order[other_scc_id].after.add(scc_id)
|
|
for other_scc_id in order[scc_id].after:
|
|
order[other_scc_id].before.add(scc_id)
|
|
|
|
return scc
|
|
|
|
def solve_groups(self, groups):
|
|
result = []
|
|
emitted = set()
|
|
queue = deque()
|
|
key2item = {}
|
|
debug_solver = self.logger.isEnabledFor(logging.DEBUG)
|
|
|
|
order = self._compute_group_order(groups, key2item)
|
|
|
|
# === MILESTONE: Partial-order constrains computed ===
|
|
|
|
# At this point, we have computed all the partial-order
|
|
# constrains needed. Some of these may have created strongly
|
|
# connected components (SSC) [of size 2 or greater], which
|
|
# represents a group of items that (we believe) must migrate
|
|
# together.
|
|
#
|
|
# Each one of those components will become an "easy" hint.
|
|
|
|
comps = compute_scc(order)
|
|
# Now that we got the SSCs (in comps), we select on item from
|
|
# each SSC to represent the group and become an ID for that
|
|
# SSC.
|
|
# * scc_keys[ssc_id] => All the item-keys in that SSC
|
|
#
|
|
# We also "repair" the ordering, so we know in which order the
|
|
# hints should be emitted.
|
|
scc_keys = self._merge_items_into_components(comps, order)
|
|
|
|
if debug_solver: # pragma: no cover
|
|
self.logger.debug("-- PARTIAL ORDER --")
|
|
|
|
initial_round = []
|
|
for com in sorted(order):
|
|
if debug_solver and order[com].before: # pragma: no cover
|
|
self.logger.debug("N: %s <= %s", com, str(sorted(order[com].before)))
|
|
if not order[com].after:
|
|
# This component can be scheduled immediately, add it
|
|
# to the queue
|
|
initial_round.append(com)
|
|
elif debug_solver: # pragma: no cover
|
|
self.logger.debug("N: %s >= %s", com, str(sorted(order[com].after)))
|
|
|
|
queue.extend(sorted(initial_round, key=len))
|
|
del initial_round
|
|
|
|
if debug_solver: # pragma: no cover
|
|
self.logger.debug("-- END PARTIAL ORDER --")
|
|
self.logger.debug("-- LINEARIZED ORDER --")
|
|
|
|
for cur in iter_except(queue.popleft, IndexError):
|
|
if order[cur].after <= emitted and cur not in emitted:
|
|
# This item is ready to be emitted right now
|
|
if debug_solver: # pragma: no cover
|
|
self.logger.debug("%s -- %s", cur, sorted(scc_keys[cur]))
|
|
emitted.add(cur)
|
|
result.append([key2item[x] for x in scc_keys[cur]])
|
|
if order[cur].before:
|
|
# There are components that come after this one.
|
|
# Add it to queue:
|
|
# - if it is ready, it will be emitted.
|
|
# - else, it will be dropped and re-added later.
|
|
queue.extend(sorted(order[cur].before - emitted, key=len))
|
|
|
|
if debug_solver: # pragma: no cover
|
|
self.logger.debug("-- END LINEARIZED ORDER --")
|
|
|
|
return result
|
|
|
|
def _dump_groups(self, groups): # pragma: no cover
|
|
self.logger.debug("=== Groups ===")
|
|
for (item, adds, rms) in groups:
|
|
self.logger.debug("%s => A: %s, R: %s", str(item), str(adds), str(rms))
|
|
self.logger.debug("=== END Groups ===")
|