#!/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 = """
{graph_content}
""" 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()