仓库提交练习
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

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()