Ticket #69314: macports_dependency_cycles.py

File macports_dependency_cycles.py, 3.5 KB (added by lukaso (Lukas Oberhuber), 8 months ago)

Script to show dependency cycles

Line 
1import subprocess
2import sys
3from collections import defaultdict
4
5
6class Graph:
7    def __init__(self):
8        self.graph = defaultdict(list)
9
10    def addEdge(self, u, v):
11        if u not in self.graph:
12            self.graph[u] = []
13        if v not in self.graph:
14            self.graph[v] = []
15        self.graph[u].append(v)
16
17    def isCyclicUtil(self, v, visited, recStack, path):
18        if v not in visited:
19            visited[v] = False
20        if v not in recStack:
21            recStack[v] = False
22
23        visited[v] = True
24        recStack[v] = True
25        path.append(v)
26
27        for neighbour in self.graph[v]:
28            if not visited.get(neighbour, False):
29                cycle = self.isCyclicUtil(neighbour, visited, recStack, path)
30                if cycle:  # If a cycle is found deeper in the recursion, bubble it up.
31                    return cycle
32            elif recStack.get(neighbour, False):
33                # Found a cycle, return the path leading to the cycle and including the cycle start point
34                cycle_start_index = path.index(neighbour)
35                cycle_path = path[cycle_start_index:]
36                cycle_path.append(
37                    neighbour
38                )  # Include the first dependency again at the end
39                return cycle_path
40
41        path.pop()  # Remove the current node from the path as we backtrack
42        recStack[v] = False
43        return None
44
45    def findCycle(self):
46        visited = {}
47        recStack = {}
48
49        for node in self.graph.keys():
50            if node not in visited:
51                cycle = self.isCyclicUtil(node, visited, recStack, [])
52                if cycle:
53                    return cycle
54        return None
55
56
57def get_dependencies(port_command, package):
58    command = f"{port_command} deps {package}"
59    process = subprocess.run(
60        command, shell=True, check=True, stdout=subprocess.PIPE, universal_newlines=True
61    )
62    output = process.stdout
63    dependencies = []
64    for line in output.split("\n"):
65        if any(
66            prefix in line
67            for prefix in [
68                "Extract Dependencies:",
69                "Build Dependencies:",
70                "Library Dependencies:",
71                "Runtime Dependencies:",
72            ]
73        ):
74            # if any(prefix in line for prefix in ["Extract Dependencies:", "Build Dependencies:"]):
75            deps = line.split(":", 1)[1].strip().split(", ")
76            dependencies.extend(deps)
77    return dependencies
78
79
80def build_graph(port_command, root_package):
81    g = Graph()
82    stack = [root_package]
83    visited = set()
84
85    while stack:
86        current_package = stack.pop()
87        if current_package not in visited:
88            visited.add(current_package)
89            deps = get_dependencies(port_command, current_package)
90            for dep in deps:
91                if (
92                    dep and not dep.isspace()
93                ):  # Check if dep is not empty or just whitespace
94                    g.addEdge(current_package, dep)
95                    if dep not in visited:
96                        stack.append(dep)
97    return g
98
99
100# Example usage
101if __name__ == "__main__":
102    if len(sys.argv) != 3:
103        print("Usage: python detect_cycles.py <path_to_port_command> <root_package>")
104        sys.exit(1)
105
106    port_command = sys.argv[1]
107    root_package = sys.argv[2]
108    g = build_graph(port_command, root_package)
109    cycle = g.findCycle()
110    if cycle:
111        print("Graph has a circular dependency:", " -> ".join(cycle))
112    else:
113        print("No circular dependencies found")