171 lines
6.8 KiB
Python
171 lines
6.8 KiB
Python
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
|