仓库提交练习
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

617 lines
24 KiB

3 weeks ago
import os
import re
import sys
import json
from collections import defaultdict
from typing import Dict, Set, List, Tuple
# ================= 配置区域 =================
IGNORE_DIRS = {'library', 'main', 'build', 'bin', '.git', 'cmake'}
TARGET_FILE = 'CMakeLists.txt'
OUTPUT_MD = 'Mermaid.md'
OUTPUT_HTML = 'Mermaid.html'
# ===========================================
def find_cmake_files(root_path: str) -> Dict[str, str]:
valid_modules = {}
print(f"🔍 开始扫描目录: {root_path} ...")
for dirpath, dirnames, filenames in os.walk(root_path):
current_dir_name = os.path.basename(dirpath)
if dirpath == root_path:
continue
if current_dir_name in IGNORE_DIRS:
continue
cmake_count = filenames.count(TARGET_FILE)
if cmake_count == 1:
valid_modules[dirpath] = current_dir_name
elif cmake_count > 1:
print(f"⚠️ 跳过异常目录 (存在多个 {TARGET_FILE}): {dirpath}")
print(f"✅ 找到 {len(valid_modules)} 个有效模块。\n")
return valid_modules
def parse_dependencies(modules: Dict[str, str]) -> Dict[str, Set[str]]:
dependencies = defaultdict(set)
module_names = set(modules.values())
tll_pattern = re.compile(r'target_link_libraries\s*\(\s*[\w\d_]+\s*(?:PUBLIC|PRIVATE|INTERFACE)?\s*(.*?)\)', re.IGNORECASE | re.DOTALL)
lib_name_pattern = re.compile(r'[\w\d_\-\.]+')
print("📝 正在分析依赖关系...")
for path, mod_name in modules.items():
file_path = os.path.join(path, TARGET_FILE)
try:
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
matches = tll_pattern.findall(content)
for match_group in matches:
libs = lib_name_pattern.findall(match_group)
for lib in libs:
if lib in module_names and lib != mod_name:
dependencies[mod_name].add(lib)
except Exception as e:
print(f"❌ 读取文件失败 {file_path}: {e}")
return dependencies
def generate_mermaid_content(dependencies: Dict[str, Set[str]], all_modules: Set[str]) -> str:
graph_lines = ["graph TD"]
nodes_in_edges = set()
edges = []
for src, targets in dependencies.items():
nodes_in_edges.add(src)
for tgt in targets:
nodes_in_edges.add(tgt)
edges.append(f" {src} --> {tgt}")
if edges:
graph_lines.extend(edges)
isolated_nodes = all_modules - nodes_in_edges
if isolated_nodes:
for node in sorted(isolated_nodes):
graph_lines.append(f" {node}")
mermaid_graph_code = "\n".join(graph_lines)
header = "# CMake 模块依赖图\n\n以下是自动生成的模块依赖关系图:\n\n"
block_start = "```mermaid\n"
block_end = "\n```\n"
footer = f"\n> 提示:已生成离线交互图表 `{OUTPUT_HTML}`。\n"
return header + block_start + mermaid_graph_code + block_end + footer
def generate_offline_html(dependencies: Dict[str, Set[str]], all_modules: Set[str]):
nodes_list = sorted(list(all_modules))
edges_list = []
# 统计每个节点的入度和出度,用于 Tooltip 显示
in_degree = defaultdict(int)
out_degree = defaultdict(int)
for src, targets in dependencies.items():
out_degree[src] = len(targets)
for tgt in targets:
in_degree[tgt] += 1
edges_list.append({"source": src, "target": tgt})
# 构建包含统计信息的节点数据
nodes_data = []
for name in nodes_list:
nodes_data.append({
"id": name,
"in": in_degree.get(name, 0),
"out": out_degree.get(name, 0)
})
data_json = json.dumps({
"nodes": nodes_data,
"edges": edges_list
})
html_template = """<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CMake 依赖图 (终极版)</title>
<style>
body { margin: 0; overflow: hidden; background-color: #f4f6f8; font-family: 'Segoe UI', sans-serif; }
#container { width: 100vw; height: 100vh; position: relative; }
svg { width: 100%; height: 100%; cursor: grab; display: block; }
svg:active { cursor: grabbing; }
/* 节点样式 */
.node circle { fill: #fff; stroke: #555; stroke-width: 2px; transition: all 0.3s; }
.node text { font-size: 13px; font-weight: 600; pointer-events: none; text-anchor: middle; dy: 5; fill: #333; user-select: none; }
/* 连线样式 */
.link { stroke: #ccc; stroke-opacity: 0.4; stroke-width: 1.5px; fill: none; }
.link-arrow { fill: #ccc; opacity: 0.4; }
/* 交互状态 */
.node.highlight circle { fill: #d4edda; stroke: #28a745; stroke-width: 3px; r: 35; }
.node.highlight text { fill: #155724; font-weight: bold; }
.node.dimmed { opacity: 0.15; }
.node.dimmed circle { stroke: #999; }
.link.dimmed { opacity: 0.05; stroke-width: 1px; }
.link.highlight { stroke: #28a745; stroke-width: 2.5px; opacity: 1; }
.link-arrow.highlight { fill: #28a745; opacity: 1; }
/* UI 控件 */
#ui-layer {
position: absolute; top: 20px; left: 20px; z-index: 10;
display: flex; flex-direction: column; gap: 10px;
}
.panel {
background: rgba(255,255,255,0.95);
padding: 15px; border-radius: 8px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
backdrop-filter: blur(5px);
}
h2 { margin: 0 0 5px 0; font-size: 16px; color: #333; }
p { margin: 0; font-size: 12px; color: #666; line-height: 1.4; }
/* 搜索框 */
#search-box {
padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px;
width: 200px; font-size: 14px; outline: none; transition: border 0.2s;
}
#search-box:focus { border-color: #28a745; }
/* 按钮 */
.btn {
background: #007bff; color: white; border: none;
padding: 8px 15px; border-radius: 4px; cursor: pointer;
font-size: 13px; margin-top: 5px; width: 100%;
transition: background 0.2s;
}
.btn:hover { background: #0056b3; }
.btn-secondary { background: #6c757d; }
.btn-secondary:hover { background: #545b62; }
/* Tooltip */
#tooltip {
position: absolute; background: rgba(0,0,0,0.8); color: #fff;
padding: 8px 12px; border-radius: 4px; font-size: 12px;
pointer-events: none; opacity: 0; transition: opacity 0.2s;
z-index: 20; white-space: pre-line; max-width: 200px;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
}
#tooltip strong { color: #4facfe; }
</style>
</head>
<body>
<div id="container">
<!-- UI -->
<div id="ui-layer">
<div class="panel">
<h2>📦 CMake 依赖图</h2>
<p>💡 点击节点查看<strong>对外依赖</strong></p>
<input type="text" id="search-box" placeholder="🔍 搜索模块名称...">
<button class="btn btn-secondary" id="btn-export">📷 导出 PNG 图片</button>
</div>
<div class="panel" id="stats-panel" style="display:none;">
<p id="stats-content"></p>
</div>
</div>
<!-- 画布 -->
<svg id="graph-svg"></svg>
<!-- 悬浮提示 -->
<div id="tooltip"></div>
</div>
<script>
const rawData = {data_json_placeholder};
const svg = document.getElementById('graph-svg');
const tooltip = document.getElementById('tooltip');
const searchBox = document.getElementById('search-box');
const btnExport = document.getElementById('btn-export');
const statsPanel = document.getElementById('stats-panel');
const statsContent = document.getElementById('stats-content');
let width = window.innerWidth;
let height = window.innerHeight;
// 视图变换
let transform = { x: 0, y: 0, k: 1 };
let isDragging = false;
let startPan = { x: 0, y: 0 };
const gLinks = document.createElementNS("http://www.w3.org/2000/svg", "g");
const gNodes = document.createElementNS("http://www.w3.org/2000/svg", "g");
const gArrows = document.createElementNS("http://www.w3.org/2000/svg", "defs");
// 定义箭头标记
gArrows.innerHTML = `
<marker id="arrow-default" markerWidth="10" markerHeight="10" refX="28" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,6 L9,3 z" fill="#ccc" opacity="0.4" />
</marker>
<marker id="arrow-highlight" markerWidth="10" markerHeight="10" refX="33" refY="3" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,6 L9,3 z" fill="#28a745" />
</marker>
`;
svg.appendChild(gArrows);
svg.appendChild(gLinks);
svg.appendChild(gNodes);
// 初始化节点数据
const nodes = rawData.nodes.map(n => ({
id: n.id,
inDeg: n.in,
outDeg: n.out,
x: Math.random() * (width - 100) + 50,
y: Math.random() * (height - 100) + 50,
vx: 0, vy: 0,
radius: 25
}));
const nodeMap = {};
nodes.forEach(n => nodeMap[n.id] = n);
const links = rawData.edges.map(e => ({
source: nodeMap[e.source],
target: nodeMap[e.target]
}));
// === 物理引擎参数 ===
const REPULSION = 6000;
const SPRING_LENGTH = 250;
const SPRING_STRENGTH = 0.04;
const DAMPING = 0.85;
const CENTER_GRAVITY = 0.01;
function tick() {
// 斥力
for (let i = 0; i < nodes.length; i++) {
for (let j = i + 1; j < nodes.length; j++) {
const a = nodes[i], b = nodes[j];
let dx = a.x - b.x, dy = a.y - b.y;
let dist = Math.sqrt(dx*dx + dy*dy) || 1;
let force = REPULSION / (dist * dist);
a.vx += (dx / dist) * force;
a.vy += (dy / dist) * force;
b.vx -= (dx / dist) * force;
b.vy -= (dy / dist) * force;
}
}
// 引力
links.forEach(link => {
const a = link.source, b = link.target;
let dx = b.x - a.x, dy = b.y - a.y;
let dist = Math.sqrt(dx*dx + dy*dy) || 1;
let force = (dist - SPRING_LENGTH) * SPRING_STRENGTH;
let fx = (dx / dist) * force, fy = (dy / dist) * force;
a.vx += fx; a.vy += fy;
b.vx -= fx; b.vy -= fy;
});
// 更新位置
nodes.forEach(n => {
n.vx += (width/2 - n.x) * CENTER_GRAVITY;
n.vy += (height/2 - n.y) * CENTER_GRAVITY;
n.x += n.vx; n.y += n.vy;
n.vx *= DAMPING; n.vy *= DAMPING;
// 边界
n.x = Math.max(n.radius, Math.min(width - n.radius, n.x));
n.y = Math.max(n.radius, Math.min(height - n.radius, n.y));
});
}
let iterations = 0;
function animate() {
if (iterations < 400) {
tick();
iterations++;
render();
requestAnimationFrame(animate);
} else {
// 动画结束不再每帧重绘 DOM只在交互时更新
}
}
function render() {
// 渲染连线 (带箭头)
let linksHTML = '';
links.forEach((l, idx) => {
linksHTML += `<line x1="${l.source.x}" y1="${l.source.y}" x2="${l.target.x}" y2="${l.target.y}"
class="link" data-src="${l.source.id}" data-tgt="${l.target.id}"
marker-end="url(#arrow-default)" />`;
});
gLinks.innerHTML = linksHTML;
// 渲染节点
let nodesHTML = '';
nodes.forEach(n => {
nodesHTML += `
<g class="node" transform="translate(${n.x},${n.y})" data-id="${n.id}">
<circle r="${n.radius}" />
<text>${n.id}</text>
</g>
`;
});
gNodes.innerHTML = nodesHTML;
bindEvents();
}
function bindEvents() {
// 节点交互
document.querySelectorAll('.node').forEach(el => {
el.addEventListener('click', (e) => {
e.stopPropagation();
const id = el.getAttribute('data-id');
highlight(id);
});
el.addEventListener('mouseenter', (e) => {
const id = el.getAttribute('data-id');
const node = nodeMap[id];
tooltip.innerHTML = `<strong>${id}</strong><br>被依赖: ${node.inDeg}<br>依赖他人: ${node.outDeg}`;
tooltip.style.opacity = 1;
moveTooltip(e);
});
el.addEventListener('mousemove', moveTooltip);
el.addEventListener('mouseleave', () => {
tooltip.style.opacity = 0;
});
});
// 全局鼠标移动 (用于拖拽和 Tooltip)
document.addEventListener('mousemove', (e) => {
if(isDragging) {
transform.x = e.clientX - startPan.x;
transform.y = e.clientY - startPan.y;
updateTransform();
}
if(tooltip.style.opacity == 1) moveTooltip(e);
});
}
function moveTooltip(e) {
tooltip.style.left = (e.clientX + 15) + 'px';
tooltip.style.top = (e.clientY + 15) + 'px';
}
// === 核心高亮逻辑只高亮输出 ===
function highlight(activeId) {
document.querySelectorAll('.node').forEach(n => n.classList.remove('highlight', 'dimmed'));
document.querySelectorAll('.link').forEach(l => {
l.classList.remove('highlight', 'dimmed');
l.setAttribute('marker-end', 'url(#arrow-default)');
});
const relatedIds = new Set([activeId]);
let count = 0;
document.querySelectorAll('.link').forEach(l => {
const src = l.getAttribute('data-src');
const tgt = l.getAttribute('data-tgt');
if (src === activeId) {
relatedIds.add(tgt);
l.classList.add('highlight');
l.setAttribute('marker-end', 'url(#arrow-highlight)'); // 变色箭头
count++;
} else {
l.classList.add('dimmed');
}
});
document.querySelectorAll('.node').forEach(n => {
const id = n.getAttribute('data-id');
if (relatedIds.has(id)) {
n.classList.add('highlight');
} else {
n.classList.add('dimmed');
}
});
// 更新统计面板
statsPanel.style.display = 'block';
statsContent.innerHTML = `选中<strong>${activeId}</strong><br>正在展示其 <span style="color:#28a745;font-weight:bold">${count}</span> 个直接依赖项`;
}
// === 搜索功能 ===
searchBox.addEventListener('input', (e) => {
const val = e.target.value.toLowerCase();
if (!val) {
// 清空搜索重置视图
document.querySelectorAll('.node').forEach(n => n.classList.remove('highlight', 'dimmed'));
document.querySelectorAll('.link').forEach(l => {
l.classList.remove('highlight', 'dimmed');
l.setAttribute('marker-end', 'url(#arrow-default)');
});
statsPanel.style.display = 'none';
return;
}
// 简单搜索高亮包含关键词的节点
let found = false;
document.querySelectorAll('.node').forEach(n => {
const id = n.getAttribute('data-id');
if (id.toLowerCase().includes(val)) {
n.classList.add('highlight');
n.classList.remove('dimmed');
found = true;
// 可选自动平移到第一个匹配项 (略复杂暂省略)
} else {
n.classList.add('dimmed');
n.classList.remove('highlight');
}
});
// 淡化所有连线
document.querySelectorAll('.link').forEach(l => l.classList.add('dimmed'));
if(found) {
statsPanel.style.display = 'block';
statsContent.innerHTML = `搜索结果"<strong>${val}</strong>"`;
} else {
statsPanel.style.display = 'none';
}
});
// === 导出图片功能 ===
btnExport.addEventListener('click', () => {
// 1. 克隆 SVG
const svgEl = document.getElementById('graph-svg');
const clone = svgEl.cloneNode(true);
// 2. 移除 transform应用当前坐标到元素本身 (简化处理直接序列化当前状态)
// 注意为了简单我们直接截取当前视口看到的区域可能更准确
// 但这里我们导出全图
const serializer = new XMLSerializer();
let source = serializer.serializeToString(clone);
// 添加命名空间
if(!source.match(/^<svg[^>]+xmlns="http\:\/\/www\.w3\.org\/2000\/svg"/)){
source = source.replace(/^<svg/, '<svg xmlns="http://www.w3.org/2000/svg"');
}
if(!source.match(/^<svg[^>]+xmlns:xlink="http\:\/\/www\.w3\.org\/1999\/xlink"/)){
source = source.replace(/^<svg/, '<svg xmlns:xlink="http://www.w3.org/1999/xlink"');
}
// 3. 创建 Canvas
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
// 填充背景
ctx.fillStyle = '#f4f6f8';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const img = new Image();
const svgBlob = new Blob([source], {type: 'image/svg+xml;charset=utf-8'});
const url = URL.createObjectURL(svgBlob);
img.onload = function() {
// 应用当前的缩放和平移
ctx.setTransform(transform.k, 0, 0, transform.k, transform.x, transform.y);
ctx.drawImage(img, 0, 0);
// 下载
const pngUrl = canvas.toDataURL('image/png');
const downloadLink = document.createElement('a');
downloadLink.href = pngUrl;
downloadLink.download = 'cmake_deps_graph.png';
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
URL.revokeObjectURL(url);
};
img.src = url;
});
// 拖拽与缩放事件绑定
svg.addEventListener('mousedown', (e) => {
if (e.target.tagName === 'svg' || e.target.tagName === 'g') {
isDragging = true;
startPan = { x: e.clientX - transform.x, y: e.clientY - transform.y };
svg.style.cursor = 'grabbing';
}
});
window.addEventListener('mouseup', () => { isDragging = false; svg.style.cursor = 'grab'; });
svg.addEventListener('wheel', (e) => {
e.preventDefault();
const scaleAmount = -e.deltaY * 0.001;
transform.k = Math.min(Math.max(0.1, transform.k + scaleAmount), 5);
updateTransform();
});
function updateTransform() {
gLinks.setAttribute('transform', `translate(${transform.x},${transform.y}) scale(${transform.k})`);
gNodes.setAttribute('transform', `translate(${transform.x},${transform.y}) scale(${transform.k})`);
}
window.addEventListener('resize', () => {
width = window.innerWidth;
height = window.innerHeight;
});
// 启动
animate();
// 初始居中偏移
transform.x = width * 0.1;
transform.y = height * 0.1;
updateTransform();
</script>
</body>
</html>
"""
final_html = html_template.replace("{data_json_placeholder}", data_json)
return final_html
def print_tree_view(dependencies: Dict[str, Set[str]], all_modules: Set[str]):
# (保持原有逻辑不变)
print("\n" + "="*50)
print("🌳 终端依赖树状图:")
print("="*50)
if not all_modules:
print(" (无模块)")
return
children = set()
for targets in dependencies.values(): children.update(targets)
roots = all_modules - children
if not roots and all_modules: roots = {sorted(all_modules)[0]}
sorted_roots = sorted(roots)
if not sorted_roots and all_modules: sorted_roots = [sorted(all_modules)[0]]
for i, root in enumerate(sorted_roots):
is_last = (i == len(sorted_roots) - 1)
visited_in_tree = set()
def _recurse(n: str, pre: str, last: bool, visited: set):
conn = "└── " if last else "├── "
print(f"{pre}{conn}{n}")
if n in visited:
print(f"{pre}{' ' if last else ''} (↑ 循环引用)")
return
visited.add(n)
sub_deps = sorted(dependencies.get(n, set()))
if not sub_deps: return
new_pre = pre + (" " if last else "")
for idx, sub in enumerate(sub_deps):
_recurse(sub, new_pre, idx == len(sub_deps)-1, visited)
_recurse(root, "", is_last, visited_in_tree)
print("="*50 + "\n")
def main():
root_dir = os.path.abspath(sys.argv[1]) if len(sys.argv) > 1 else os.getcwd()
if not os.path.isdir(root_dir):
print(f"错误:目录不存在 - {root_dir}")
sys.exit(1)
modules = find_cmake_files(root_dir)
deps_map = parse_dependencies(modules) if modules else {}
all_mods = set(modules.values()) if modules else set()
print_tree_view(deps_map, all_mods)
md_content = generate_mermaid_content(deps_map, all_mods)
md_path = os.path.join(root_dir, OUTPUT_MD)
with open(md_path, 'w', encoding='utf-8') as f:
f.write(md_content)
html_content = generate_offline_html(deps_map, all_mods)
html_path = os.path.join(root_dir, OUTPUT_HTML)
with open(html_path, 'w', encoding='utf-8') as f:
f.write(html_content)
print(f"🎉 完成!")
print(f" 📄 文档图表:{md_path}")
print(f" 🌐 交互图表:{html_path}")
print(f" ✨ 新增功能:搜索框、悬停详情、一键导出 PNG")
if __name__ == "__main__":
main()