diff --git a/analyze-repodb.py b/analyze-repodb.py index de79c2b..be1d920 100755 --- a/analyze-repodb.py +++ b/analyze-repodb.py @@ -6,7 +6,6 @@ import sys import gettext import argparse import sqlite3 -import string import rpm import re @@ -591,6 +590,116 @@ SELECT srp.name, sp.nvra, tp.nvra, tp.repodir_id FROM packages sp, rpm_provides return (result, len(result)) +class cycles(query_output): + title = 'Dependency cycles' + + + def find_all_cycles(self, graph): + """ + Find all cycles in the given graph. + + This function will return a list of lists of nodes, which form cycles + in the graph or an empty list if no cycle exists. + """ + + def dfs(node): + """ + Depth-first search subfunction. + """ + + def find_cycle_to_ancestor(spanning_tree, node, ancestor): + """ + Find a cycle containing both node and ancestor. + """ + path = [] + while (node != ancestor): + if node is None: + return [] + path.append(node) + node = spanning_tree[node] + path.append(node) + path.reverse() + return path + + visited.add(node) + # Explore recursively the connected component + if node not in graph: + return + for each in graph[node]: + if each not in visited: + spanning_tree[each] = node + dfs(each) + else: + if (spanning_tree[node] != each): + cycle = find_cycle_to_ancestor(spanning_tree, node, + each) + if cycle: + cycles.append(cycle) + + visited = set() # List for marking visited and non-visited nodes + spanning_tree = {} # Spanning tree + cycles = [] + + # Algorithm outer-loop + for each in graph: + # Select a non-visited node + if each not in visited: + spanning_tree[each] = None + # Explore node's connected component + dfs(each) + return cycles + + def get_data(self, repodir_id): + rows = self.dbc.execute(""" +SELECT sp.id, tp.id, sp.nvra, tp.nvra, req.name + FROM packages sp + CROSS JOIN package_requires_res pqr ON sp.id = pqr.package_id + CROSS JOIN rpm_requires req ON pqr.requires_id = req.id + CROSS JOIN packages tp ON pqr.dep_package_id = tp.id + WHERE sp.repodir_id = ? AND tp.repodir_id = sp.repodir_id AND sp.id <> tp.id + ORDER BY sp.id, tp.id +""", [repodir_id]).fetchall() + pre_pkg_id = None + pre_dep_pkg_id = None + deps = [] + graph = {} + pkg_names = {} + req_links = {} + for row in rows: + (pkg_id, dep_pkg_id) = (row[0], row[1]) + pkg_names[row[0]] = row[2] + pkg_names[row[1]] = row[3] + if pre_pkg_id is not None and pre_pkg_id != pkg_id: + graph[pre_pkg_id] = deps + deps = [] + if (pkg_id, dep_pkg_id) not in req_links: + req_links[(pkg_id, dep_pkg_id)] = [] + req_links[(pkg_id, dep_pkg_id)].append(row[4]) + if pkg_id == pre_pkg_id and dep_pkg_id == pre_dep_pkg_id: + continue + deps.append(dep_pkg_id) + pre_pkg_id = pkg_id + pre_dep_pkg_id = dep_pkg_id + if pre_pkg_id is not None: + graph[pre_pkg_id] = deps + + scc = self.find_all_cycles(graph) + result = [] + if scc is not None: + for comp in scc: + if len(comp) < 2: + continue + cycle_str = pkg_names[comp[0]] + for n0 in xrange(0, len(comp)): + n1 = n0 + 1 if n0 + 1 < len(comp) else 0 + if (comp[n0], comp[n1]) in req_links: + pass + cycle_str += ' =(%s)=> %s' % ( + ';'.join(req_links[(comp[n0], comp[n1])]), + pkg_names[comp[n1]]) + result.append(cycle_str) + return (result, len(result)) + def main(args): options = parseargs() @@ -610,6 +719,7 @@ def main(args): symbols_not_resolved(dbc).print_text() file_conflicts(dbc).print_text() provides_conflicts(dbc).print_text() + cycles(dbc).print_text() conn.close() if __name__ == "__main__":