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 = """ CMake 依赖图 (终极版)

📦 CMake 依赖图

💡 点击节点查看对外依赖

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