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