Created
May 25, 2025 02:53
-
-
Save JohnScience/d2049e5d62a8fadac34c793db177dd6d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import sys | |
import re | |
def parse_tree(lines): | |
""" | |
Parse the cargo tree lines into a graph. | |
Returns: | |
nodes: set of node ids (name@version) | |
edges: list of (parent, child) pairs | |
""" | |
stack = [] # stack of (indent_level, node_id) | |
nodes = set() | |
edges = [] | |
# Regex to extract dependency: e.g. anyhow v1.0.98 or anyhow v1.0.98 (proc-macro) | |
dep_re = re.compile(r"^\s*[├└]──\s+([^ ]+)\s+(v[\d\.]+)(?:\s+\(.+\))?") | |
# First line is the root, special handling | |
root_line = lines[0].strip() | |
# Root might have path in parentheses, strip after space | |
root_match = re.match(r"^([^ ]+)\s+(v[\d\.]+).*$", root_line) | |
if root_match: | |
root_name = root_match.group(1) | |
root_version = root_match.group(2) | |
root_id = f"{root_name}@{root_version}" | |
else: | |
raise ValueError("Could not parse root dependency line: " + root_line) | |
nodes.add(root_id) | |
stack.append( (0, root_id) ) | |
for line in lines[1:]: | |
if not line.strip(): | |
continue | |
# Count indent level: each indent is 4 spaces or a single vertical line + spaces | |
# After the root line, each dependency line starts with some prefix like "│ " or " " | |
# Simplify by counting bytes before '├' or '└' (first occurrence) | |
m = re.search(r"[├└]──", line) | |
if not m: | |
continue # skip lines without dependency marker | |
pos = m.start() | |
# Indent level estimate: pos // 4 | |
indent_level = pos // 4 | |
dep_match = dep_re.match(line) | |
if not dep_match: | |
# e.g. lines like [build-dependencies], skip | |
continue | |
name = dep_match.group(1) | |
version = dep_match.group(2) | |
node_id = f"{name}@{version}" | |
nodes.add(node_id) | |
# Find parent with indent_level-1 | |
# Pop from stack until top has indent_level < current | |
while stack and stack[-1][0] >= indent_level: | |
stack.pop() | |
if stack: | |
parent_id = stack[-1][1] | |
edges.append( (parent_id, node_id) ) | |
else: | |
# No parent found, treat as root's child | |
edges.append( (root_id, node_id) ) | |
stack.append( (indent_level, node_id) | |
) | |
return nodes, edges | |
def sanitize_label(label): | |
# Escape quotes, backslashes for DOT labels | |
return label.replace("\\", "\\\\").replace("\"", "\\\"") | |
def generate_dot(nodes, edges, graph_name="cargo_tree"): | |
dot = [] | |
dot.append(f'digraph "{graph_name}" {{') | |
dot.append(' rankdir=LR;') | |
dot.append(' node [shape=box, style=filled, fillcolor="#EEEEEE"];') | |
# Define nodes | |
for node in nodes: | |
# node is like pkg@version | |
label = sanitize_label(node) | |
dot.append(f' "{node}" [label="{label}"];') | |
# Define edges | |
for parent, child in edges: | |
# we invert the direction to make it more readable | |
dot.append(f' "{child}" -> "{parent}";') | |
dot.append('}') | |
return "\n".join(dot) | |
def main(): | |
if len(sys.argv) != 2: | |
print("Usage: python cargo-tree-to-dot.py <cargo-tree-output-file>", file=sys.stderr) | |
sys.exit(1) | |
filename = sys.argv[1] | |
with open(filename, "r", encoding="utf-8") as f: | |
lines = f.readlines() | |
nodes, edges = parse_tree(lines) | |
dot_output = generate_dot(nodes, edges) | |
print(dot_output) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Better alternative: https://github.com/jplatte/cargo-depgraph