Skip to content

Instantly share code, notes, and snippets.

@JohnScience
Created May 25, 2025 02:53
Show Gist options
  • Save JohnScience/d2049e5d62a8fadac34c793db177dd6d to your computer and use it in GitHub Desktop.
Save JohnScience/d2049e5d62a8fadac34c793db177dd6d to your computer and use it in GitHub Desktop.
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()
@JohnScience
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment