import hashlib import os import ast def compute_sha1(content: str) -> str: return hashlib.sha1(content.encode('utf-8')).hexdigest()[:16] def _check_annotation_for_export(annotation) -> bool: """递归检查类型注解中是否包含 CExport""" if annotation is None: return False if isinstance(annotation, ast.Attribute): if hasattr(annotation, 'attr') and annotation.attr == 'CExport': return True elif isinstance(annotation, ast.BinOp) and isinstance(annotation.op, ast.BitOr): return _check_annotation_for_export(annotation.left) or _check_annotation_for_export(annotation.right) elif isinstance(annotation, ast.Name): return annotation.id == 'CExport' return False def sha1_file(filepath: str) -> str: with open(filepath, 'r', encoding='utf-8') as f: content = f.read() return compute_sha1(content) def get_file_dependencies(filepath: str, src_root: str) -> set: """解析 Python 文件的导入依赖""" dependencies = set() try: with open(filepath, 'r', encoding='utf-8') as f: content = f.read() tree = ast.parse(content) for node in ast.walk(tree): if isinstance(node, ast.Import): for alias in node.names: module = alias.name dependencies.add(module) elif isinstance(node, ast.ImportFrom): if node.module: module = node.module if node.level > 0: # from .module import name → 依赖 pkg.module(模块本身) pkg_dir = os.path.dirname(filepath) pkg_rel = os.path.relpath(pkg_dir, src_root).replace(os.sep, '.').replace('/', '.') if pkg_rel == '.': pkg_rel = '' if pkg_rel: dep_module = f'{pkg_rel}.{module}' else: dep_module = module dependencies.add(dep_module) else: if not module.startswith('_'): dependencies.add(module) elif node.level > 0 and node.names: # from . import name → 依赖 pkg.name(子模块) for alias in node.names: pkg_dir = os.path.dirname(filepath) pkg_rel = os.path.relpath(pkg_dir, src_root).replace(os.sep, '.').replace('/', '.') if pkg_rel == '.': pkg_rel = '' dep_module = f'{pkg_rel}.{alias.name}' if pkg_rel else alias.name dependencies.add(dep_module) except Exception as _e: from lib.core.VLogger import get_logger as _vlog from lib.constants.config import mode as _ConfigMode if _ConfigMode == "strict": raise _vlog().warning(f"解析文件导入依赖失败: {_e}", "Exception") return dependencies def find_reachable_files_from_entries(src_root: str, entry_files: list) -> set: """从入口文件出发,找出所有可达的 .py 文件(通过导入关系)""" all_files = {} # module_name -> filepath for root, dirs, files in os.walk(src_root): dirs[:] = [d for d in dirs if d not in ('__pycache__',)] for file in files: if file.endswith('.py'): filepath = os.path.join(root, file) rel = os.path.relpath(filepath, src_root) module = os.path.splitext(rel)[0].replace(os.sep, '.').replace('/', '.') if filepath.endswith('__init__.py'): all_files[module] = filepath parent_module = module.rsplit('.', 1)[0] if '.' in module else module if parent_module not in all_files or not all_files[parent_module].endswith('__init__.py'): all_files[parent_module] = filepath elif module not in all_files: all_files[module] = filepath top_module = module.split('.')[0] if top_module not in all_files: all_files[top_module] = filepath reachable = set() queue = list(entry_files) while queue: current = queue.pop(0) if current in reachable: continue reachable.add(current) deps = get_file_dependencies(current, src_root) for dep in deps: if dep in all_files and all_files[dep] not in reachable: queue.append(all_files[dep]) else: for key, val in all_files.items(): if key.endswith('.' + dep) or key == dep: if val not in reachable: queue.append(val) break return reachable def topological_sort_files(py_files: list, src_root: str) -> list: """对 Python 文件进行拓扑排序,确保依赖模块先被处理""" # 构建模块名到文件路径的映射 module_to_file = {} for filepath in py_files: rel = os.path.relpath(filepath, src_root) module = os.path.splitext(rel)[0].replace(os.sep, '.').replace('/', '.') module_to_file[module] = filepath # 也添加顶层模块名 top_module = module.split('.')[0] if top_module not in module_to_file: module_to_file[top_module] = filepath # 构建依赖图 graph = {f: set() for f in py_files} for filepath in py_files: deps = get_file_dependencies(filepath, src_root) for dep in deps: if dep in module_to_file: dep_file = module_to_file[dep] if dep_file != filepath: # 避免自依赖 graph[filepath].add(dep_file) # 拓扑排序 (Kahn's algorithm) in_degree = {f: 0 for f in py_files} for f, deps in graph.items(): for dep in deps: if dep in in_degree: in_degree[f] += 1 queue = [f for f, degree in in_degree.items() if degree == 0] sorted_files = [] while queue: # 按字母顺序处理同级别的文件,确保确定性 queue.sort() current = queue.pop(0) sorted_files.append(current) for f, deps in graph.items(): if current in deps: in_degree[f] -= 1 if in_degree[f] == 0: queue.append(f) # 如果有环,添加剩余文件 if len(sorted_files) < len(py_files): remaining = [f for f in py_files if f not in sorted_files] sorted_files.extend(remaining) return sorted_files