backend-infra-engineer: Release v0.3.3 snapshot

This commit is contained in:
scawful
2025-11-21 21:35:50 -05:00
parent 3d71417f62
commit 476dd1cd1c
818 changed files with 65706 additions and 35514 deletions

422
scripts/visualize-deps.py Executable file
View File

@@ -0,0 +1,422 @@
#!/usr/bin/env python3
"""
Dependency Graph Visualizer
Parses CMake targets and generates dependency graphs
Usage:
python3 scripts/visualize-deps.py [build_directory] [--format FORMAT]
Formats:
- graphviz: DOT format for graphviz (default)
- mermaid: Mermaid diagram format
- text: Simple text tree
Exit codes:
0 - Success
1 - Error (build directory not found, etc.)
"""
import os
import sys
import json
import argparse
import re
from pathlib import Path
from typing import Dict, Set, List, Tuple
from collections import defaultdict
class Colors:
"""ANSI color codes"""
RESET = '\033[0m'
RED = '\033[31m'
GREEN = '\033[32m'
YELLOW = '\033[33m'
BLUE = '\033[34m'
MAGENTA = '\033[35m'
CYAN = '\033[36m'
class DependencyGraph:
"""Parse and analyze CMake dependency graph"""
def __init__(self, build_dir: Path):
self.build_dir = build_dir
self.targets: Dict[str, Set[str]] = defaultdict(set)
self.target_types: Dict[str, str] = {}
self.circular_deps: List[List[str]] = []
def parse_cmake_files(self):
"""Parse CMakeLists.txt files to extract targets and dependencies"""
print(f"{Colors.BLUE}Parsing CMake configuration...{Colors.RESET}")
# Try to parse from CMake's dependency info
dep_info_dir = self.build_dir / "CMakeFiles" / "TargetDirectories.txt"
if dep_info_dir.exists():
with open(dep_info_dir, 'r') as f:
for line in f:
line = line.strip()
if line:
# Extract target name from path
match = re.search(r'CMakeFiles/([^/]+)\.dir', line)
if match:
target_name = match.group(1)
self.targets[target_name] = set()
self.target_types[target_name] = "UNKNOWN"
# Try to extract more info from compile_commands.json
compile_commands = self.build_dir / "compile_commands.json"
if compile_commands.exists():
try:
with open(compile_commands, 'r') as f:
commands = json.load(f)
for cmd in commands:
file_path = cmd.get('file', '')
# Try to infer target from file path
if '/src/' in file_path:
parts = file_path.split('/src/')[-1].split('/')
if len(parts) > 1:
target = parts[0]
if target not in self.targets:
self.targets[target] = set()
self.target_types[target] = "LIBRARY"
except json.JSONDecodeError:
print(f"{Colors.YELLOW}⚠ Could not parse compile_commands.json{Colors.RESET}")
# Parse dependency information from generated cmake files
self._parse_cmake_depends()
print(f"{Colors.GREEN}✓ Found {len(self.targets)} targets{Colors.RESET}")
def _parse_cmake_depends(self):
"""Parse CMake depend.make files for dependency information"""
cmake_files_dir = self.build_dir / "CMakeFiles"
if not cmake_files_dir.exists():
return
# Look for depend.make files
for target_dir in cmake_files_dir.glob("*.dir"):
target_name = target_dir.name.replace('.dir', '')
depend_make = target_dir / "depend.make"
if depend_make.exists():
try:
with open(depend_make, 'r') as f:
content = f.read()
# Extract dependencies from depend.make
# Format: target_name.dir/file.cc.o: path/to/header.h
for line in content.split('\n'):
if ':' in line and not line.startswith('#'):
parts = line.split(':')
if len(parts) >= 2:
deps = parts[1].strip()
# Look for other target dependencies
for other_target in self.targets.keys():
if other_target in deps and other_target != target_name:
self.targets[target_name].add(other_target)
except Exception as e:
print(f"{Colors.YELLOW}⚠ Error parsing {depend_make}: {e}{Colors.RESET}")
# Also check link.txt for library dependencies
for target_dir in cmake_files_dir.glob("*.dir"):
target_name = target_dir.name.replace('.dir', '')
link_txt = target_dir / "link.txt"
if link_txt.exists():
try:
with open(link_txt, 'r') as f:
link_cmd = f.read()
# Parse linked libraries
for other_target in self.targets.keys():
if other_target in link_cmd and other_target != target_name:
self.targets[target_name].add(other_target)
except Exception:
pass
def detect_circular_dependencies(self) -> List[List[str]]:
"""Detect circular dependencies using DFS"""
print(f"{Colors.BLUE}Checking for circular dependencies...{Colors.RESET}")
visited = set()
rec_stack = set()
cycles = []
def dfs(node: str, path: List[str]):
visited.add(node)
rec_stack.add(node)
path.append(node)
for neighbor in self.targets.get(node, []):
if neighbor not in visited:
dfs(neighbor, path.copy())
elif neighbor in rec_stack:
# Found a cycle
cycle_start = path.index(neighbor)
cycle = path[cycle_start:] + [neighbor]
cycles.append(cycle)
rec_stack.remove(node)
for target in self.targets:
if target not in visited:
dfs(target, [])
self.circular_deps = cycles
if cycles:
print(f"{Colors.RED}✗ Found {len(cycles)} circular dependencies{Colors.RESET}")
for cycle in cycles:
print(f" {' -> '.join(cycle)}")
else:
print(f"{Colors.GREEN}✓ No circular dependencies detected{Colors.RESET}")
return cycles
def generate_graphviz(self, output_file: Path = None):
"""Generate GraphViz DOT format"""
print(f"{Colors.BLUE}Generating GraphViz diagram...{Colors.RESET}")
dot = ["digraph Dependencies {"]
dot.append(" rankdir=LR;")
dot.append(" node [shape=box, style=rounded];")
dot.append("")
# Define node styles
dot.append(" // Node definitions")
for target, target_type in self.target_types.items():
if target_type == "EXECUTABLE":
color = "lightblue"
elif target_type == "LIBRARY":
color = "lightgreen"
else:
color = "lightgray"
safe_name = target.replace("-", "_").replace("::", "_")
dot.append(f' {safe_name} [label="{target}", fillcolor={color}, style="rounded,filled"];')
dot.append("")
dot.append(" // Dependencies")
# Add edges
for target, deps in self.targets.items():
safe_target = target.replace("-", "_").replace("::", "_")
for dep in deps:
safe_dep = dep.replace("-", "_").replace("::", "_")
# Highlight circular dependencies in red
is_circular = any(
target in cycle and dep in cycle
for cycle in self.circular_deps
)
if is_circular:
dot.append(f' {safe_target} -> {safe_dep} [color=red, penwidth=2];')
else:
dot.append(f' {safe_target} -> {safe_dep};')
dot.append("}")
result = "\n".join(dot)
if output_file:
output_file.write_text(result)
print(f"{Colors.GREEN}✓ GraphViz diagram written to {output_file}{Colors.RESET}")
else:
print(result)
return result
def generate_mermaid(self, output_file: Path = None):
"""Generate Mermaid diagram format"""
print(f"{Colors.BLUE}Generating Mermaid diagram...{Colors.RESET}")
mermaid = ["graph LR"]
# Add nodes and edges
for target, deps in self.targets.items():
safe_target = target.replace("-", "_").replace("::", "_")
for dep in deps:
safe_dep = dep.replace("-", "_").replace("::", "_")
# Highlight circular dependencies
is_circular = any(
target in cycle and dep in cycle
for cycle in self.circular_deps
)
if is_circular:
mermaid.append(f' {safe_target}-->|CIRCULAR|{safe_dep}')
mermaid.append(f' style {safe_target} fill:#ff6b6b')
mermaid.append(f' style {safe_dep} fill:#ff6b6b')
else:
mermaid.append(f' {safe_target}-->{safe_dep}')
result = "\n".join(mermaid)
if output_file:
output_file.write_text(result)
print(f"{Colors.GREEN}✓ Mermaid diagram written to {output_file}{Colors.RESET}")
else:
print(result)
return result
def generate_text_tree(self, output_file: Path = None):
"""Generate simple text tree representation"""
print(f"{Colors.BLUE}Generating text tree...{Colors.RESET}")
lines = []
visited = set()
def print_tree(target: str, indent: int = 0, prefix: str = ""):
if target in visited:
lines.append(f"{prefix}├── {target} (circular)")
return
visited.add(target)
deps = list(self.targets.get(target, []))
lines.append(f"{prefix}├── {target}")
for i, dep in enumerate(deps):
is_last = i == len(deps) - 1
new_prefix = prefix + (" " if is_last else "")
print_tree(dep, indent + 1, new_prefix)
# Find root targets (targets with no incoming dependencies)
all_deps = set()
for deps in self.targets.values():
all_deps.update(deps)
roots = [t for t in self.targets.keys() if t not in all_deps]
if not roots:
# If no clear roots, just use all targets
roots = list(self.targets.keys())
lines.append("Dependency Tree:")
for root in roots[:10]: # Limit to first 10 roots
visited = set()
print_tree(root)
lines.append("")
result = "\n".join(lines)
if output_file:
output_file.write_text(result)
print(f"{Colors.GREEN}✓ Text tree written to {output_file}{Colors.RESET}")
else:
print(result)
return result
def print_statistics(self):
"""Print graph statistics"""
print(f"\n{Colors.BLUE}=== Dependency Statistics ==={Colors.RESET}")
total_targets = len(self.targets)
total_edges = sum(len(deps) for deps in self.targets.values())
avg_deps = total_edges / total_targets if total_targets > 0 else 0
print(f"Total targets: {total_targets}")
print(f"Total dependencies: {total_edges}")
print(f"Average dependencies per target: {avg_deps:.2f}")
# Find most connected targets
dep_counts = [(t, len(deps)) for t, deps in self.targets.items()]
dep_counts.sort(key=lambda x: x[1], reverse=True)
print(f"\n{Colors.CYAN}Most connected targets:{Colors.RESET}")
for target, count in dep_counts[:5]:
print(f" {target}: {count} dependencies")
# Find targets with no dependencies
isolated = [t for t, deps in self.targets.items() if len(deps) == 0]
if isolated:
print(f"\n{Colors.YELLOW}Isolated targets (no dependencies):{Colors.RESET}")
for target in isolated[:10]:
print(f" {target}")
def main():
parser = argparse.ArgumentParser(description="Visualize CMake dependency graph")
parser.add_argument(
"build_dir",
nargs="?",
default="build",
help="Build directory (default: build)"
)
parser.add_argument(
"--format",
choices=["graphviz", "mermaid", "text"],
default="graphviz",
help="Output format (default: graphviz)"
)
parser.add_argument(
"--output",
"-o",
type=Path,
help="Output file (default: stdout)"
)
parser.add_argument(
"--stats",
action="store_true",
help="Show statistics"
)
args = parser.parse_args()
build_dir = Path(args.build_dir)
if not build_dir.exists():
print(f"{Colors.RED}✗ Build directory not found: {build_dir}{Colors.RESET}")
print("Run cmake configure first: cmake --preset <preset-name>")
sys.exit(1)
if not (build_dir / "CMakeCache.txt").exists():
print(f"{Colors.RED}✗ CMakeCache.txt not found in {build_dir}{Colors.RESET}")
print("Configuration incomplete - run cmake configure first")
sys.exit(1)
print(f"{Colors.BLUE}=== CMake Dependency Visualizer ==={Colors.RESET}")
print(f"Build directory: {build_dir}\n")
graph = DependencyGraph(build_dir)
graph.parse_cmake_files()
graph.detect_circular_dependencies()
if args.stats:
graph.print_statistics()
# Generate output
output_file = args.output
if args.format == "graphviz":
if not output_file:
output_file = Path("dependencies.dot")
graph.generate_graphviz(output_file)
print(f"\nTo render: dot -Tpng {output_file} -o dependencies.png")
elif args.format == "mermaid":
if not output_file:
output_file = Path("dependencies.mmd")
graph.generate_mermaid(output_file)
print(f"\nView at: https://mermaid.live/edit")
elif args.format == "text":
graph.generate_text_tree(output_file)
print(f"\n{Colors.GREEN}✓ Dependency analysis complete{Colors.RESET}")
if __name__ == "__main__":
main()