#!/usr/bin/env python3
import subprocess
from collections import defaultdict, deque
from concurrent.futures import ThreadPoolExecutor
import threading
import os
import sys
import webbrowser
import urllib.request
MERMAID_TEMPLATE = """
"""
def ensure_local_files():
"""Ensure Mermaid.js and Panzoom.js are available locally."""
js_files = {
"mermaid.min.js": "https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js",
"panzoom.min.js": "https://cdn.jsdelivr.net/npm/panzoom@9.4.3/dist/panzoom.min.js",
}
tmp_dir = "/tmp"
for filename, url in js_files.items():
file_path = os.path.join(tmp_dir, filename)
if not os.path.exists(file_path):
print(f"Downloading {filename}...")
urllib.request.urlretrieve(url, file_path)
def preview_mermaid_graph(file_path):
"""Preview Mermaid graph in a browser."""
ensure_local_files()
if not os.path.exists(file_path):
print(f"File {file_path} does not exist.")
return
# Load the Mermaid graph content
with open(file_path, "r") as f:
graph_content = f.read()
# Prepare the HTML file content
html_content = MERMAID_TEMPLATE.format(graph_content=graph_content)
# Write the HTML to a temporary file
html_file_path = f"{file_path}.html"
with open(html_file_path, "w") as html_file:
html_file.write(html_content)
# Copy JS files to the same directory
for js_file in ["mermaid.min.js", "panzoom.min.js"]:
src = os.path.join("/tmp", js_file)
dst = os.path.join(os.path.dirname(html_file_path), js_file)
if not os.path.exists(dst):
subprocess.run(["cp", src, dst])
# Open the HTML file in the default web browser
print(f"Opening {html_file_path} in the browser...")
webbrowser.open(f"file://{os.path.abspath(html_file_path)}", new=2)
def list_installed_packages():
"""Retrieve a list of all installed packages."""
result = subprocess.run(
["dpkg-query", "-f", "${binary:Package}\n", "-W"],
stdout=subprocess.PIPE,
text=True
)
return result.stdout.strip().split("\n")
def get_package_dependencies(package):
"""Query the direct dependencies of a single package."""
result = subprocess.run(
["apt-cache", "depends", package],
stdout=subprocess.PIPE,
text=True
)
dependencies = []
for line in result.stdout.strip().split("\n"):
if line.strip().startswith("Depends:"):
dep = line.split(":", 1)[1].strip()
dep = dep.split(":")[0] # Remove parts after colon
dependencies.append(dep)
return dependencies
def build_dependency_graph(packages):
"""Build a dependency graph for the packages and save it to a file."""
graph = defaultdict(list)
lock = threading.Lock()
def process_package(package):
dependencies = get_package_dependencies(package)
with lock:
graph[package].extend(dependencies)
total_packages = len(packages)
with ThreadPoolExecutor(max_workers=20) as executor:
for i, _ in enumerate(executor.map(process_package, packages), start=1):
progress = (i / total_packages) * 100
print(f"Building dependency graph... {progress:.2f}% completed", end="\r")
output_path = "/tmp/pkg.txt"
with open(output_path, "w") as f:
for package, dependencies in graph.items():
for dep in dependencies:
f.write(f"{package}-->{dep}\n")
print(f"\nDependency graph built and saved to {output_path}")
def load_dependency_graph(file_path="/tmp/pkg.txt"):
"""Load the dependency graph from a file."""
if not os.path.exists(file_path):
raise FileNotFoundError(f"File {file_path} does not exist. Please run the build mode first.")
graph = defaultdict(list)
reverse_graph = defaultdict(list)
with open(file_path, "r") as f:
for line in f:
line = line.strip()
if "-->" in line:
source, target = line.split("-->")
graph[source].append(target)
reverse_graph[target].append(source)
return graph, reverse_graph
def trim_package_name(package):
"""Trim package name to conform to Mermaid syntax."""
return package.replace("-", "_").replace(".", "_").replace("+", "_").replace(":", "_").replace("<", "_").replace(">", "_")
def generate_mermaid_graph(graph, root_package, exclude_leaves=False):
"""Generate a Mermaid diagram syntax for the graph."""
lines = ["stateDiagram-v2"]
visited = set()
queue = deque([root_package])
is_leaf = lambda pkg: len(graph.get(pkg, [])) == 0 # Determine if it is a leaf node
while queue:
package = queue.popleft()
if package in visited:
continue
visited.add(package)
dependencies = graph.get(package, [])
for dep in dependencies:
if exclude_leaves and is_leaf(dep):
continue # Skip leaf nodes
lines.append(f" {trim_package_name(package)} --> {trim_package_name(dep)}")
if dep not in visited:
queue.append(dep)
return "\n".join(lines)
def build_mode():
print("Retrieving installed packages...")
packages = list_installed_packages()
print("Building dependency graph...")
build_dependency_graph(packages)
def depends_mode(package, exclude_leaves):
graph, _ = load_dependency_graph()
if package not in graph:
print(f"Package {package} is not in the dependency graph.")
return
print("Generating dependency graph...")
mermaid_graph = generate_mermaid_graph(graph, package, exclude_leaves)
output_file = f"{package}_depends.mmd"
with open(output_file, "w") as f:
f.write("---\n")
f.write(f"title: {package} Dependency Graph\n")
f.write("---\n\n")
f.write(mermaid_graph)
print(f"Dependency graph generated and saved as {output_file}")
preview_mermaid_graph(output_file)
def rdepends_mode(package, exclude_leaves):
_, reverse_graph = load_dependency_graph()
if package not in reverse_graph:
print(f"Package {package} is not in the reverse dependency graph.")
return
print("Generating reverse dependency graph...")
mermaid_graph = generate_mermaid_graph(reverse_graph, package, exclude_leaves)
output_file = f"{package}_rdepends.mmd"
with open(output_file, "w") as f:
f.write("---\n")
f.write(f"title: {package} Reverse Dependency Graph\n")
f.write("---\n\n")
f.write(mermaid_graph)
print(f"Reverse dependency graph generated and saved as {output_file}")
preview_mermaid_graph(output_file)
def main():
if len(sys.argv) < 2:
print("Usage: ./vispkg.py [build|depends|rdepends] [package] [--no-leaves]")
sys.exit(1)
mode = sys.argv[1]
exclude_leaves = "--no-leaves" in sys.argv
if mode == "build":
build_mode()
elif mode == "depends":
if len(sys.argv) < 3:
print("Usage: ./vispkg.py depends [--no-leaves]")
sys.exit(1)
depends_mode(sys.argv[2], exclude_leaves)
elif mode == "rdepends":
if len(sys.argv) < 3:
print("Usage: ./vispkg.py rdepends [--no-leaves]")
sys.exit(1)
rdepends_mode(sys.argv[2], exclude_leaves)
else:
print("Unknown mode. Please use: build, depends, or rdepends.")
sys.exit(1)
if __name__ == "__main__":
main()