Files
SourceCodeExtractionTool/xpr解析版/源代码提取.py
2026-07-13 17:55:17 +08:00

713 lines
25 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
import shutil
import re
import xml.etree.ElementTree as ET
from pathlib import Path
from datetime import datetime
# =================================================
# 用户配置区(唯一需要用户关心的地方)
# =================================================
def get_project_root():
"""
获取用户输入的项目根目录,并验证路径是否存在
"""
while True:
project_root = input("请输入你的工程根目录路径: ").strip()
if not project_root:
print("路径不能为空,请重新输入。")
continue
# 处理路径中的引号
if project_root.startswith('"') and project_root.endswith('"'):
project_root = project_root[1:-1]
elif project_root.startswith("'") and project_root.endswith("'"):
project_root = project_root[1:-1]
# 检查路径是否存在
if os.path.isdir(project_root):
return project_root
else:
print(f"错误: 路径 '{project_root}' 不存在,请重新输入。")
# =================================================
# Testbench 命名规则
# =================================================
TB_PATTERN = re.compile(r'(^tb_)|(_tb_)|(_tb\.)', re.IGNORECASE)
# =================================================
# 通用工具函数
# =================================================
def find_latest_xpr_in_dir(search_dir):
"""
在指定目录下查找最新修改的 xpr 文件,未找到返回 None
"""
files = [
os.path.join(search_dir, f)
for f in os.listdir(search_dir)
if f.lower().endswith('.xpr')
]
if not files:
return None
return max(files, key=os.path.getmtime)
def find_latest_file_in_dir(search_dir, extension):
"""
在指定目录下查找最新修改的指定扩展名文件,未找到返回 None
"""
if not extension.startswith("."):
extension = "." + extension
files = [
os.path.join(search_dir, f)
for f in os.listdir(search_dir)
if f.lower().endswith(extension.lower())
]
if not files:
return None
return max(files, key=os.path.getmtime)
def find_all_files_in_dir(search_dir, extension):
"""
在指定目录下查找所有指定扩展名的文件,按修改时间降序排列
"""
if not extension.startswith("."):
extension = "." + extension
files = [
os.path.join(search_dir, f)
for f in os.listdir(search_dir)
if f.lower().endswith(extension.lower())
]
files.sort(key=os.path.getmtime, reverse=True)
return files
def user_select_file(files, file_type):
"""
让用户从文件列表中选择一个,返回选中的文件路径
如果只有一个文件则自动选择
"""
if len(files) == 1:
print(f"找到 1 个 {file_type} 文件,自动选择: {os.path.basename(files[0])}")
return files[0]
print(f"请选择需要解析的 {file_type} 文件:")
for i, f in enumerate(files, 1):
print(f" {i} {os.path.basename(f)}")
while True:
try:
choice = input("请输入对应编号: ").strip()
idx = int(choice)
if 1 <= idx <= len(files):
return files[idx - 1]
print(f"编号超出范围,请输入 1 ~ {len(files)}")
except ValueError:
print("输入无效,请输入数字编号")
def load_extensions(config_path):
"""
从 config.txt 读取需要的文件后缀集合
"""
exts = set()
with open(config_path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip().lower()
if not line or line.startswith("#"):
continue
if not line.startswith("."):
line = "." + line
exts.add(line)
return exts
def is_testbench_filename(file_path):
"""
根据文件名判断是否是 testbench
"""
return bool(TB_PATTERN.search(Path(file_path).name.lower()))
# =================================================
# 模糊路径解析(核心)
# =================================================
def extract_tail_from_psrcdir(path_str):
"""
从 $PSRCDIR/xxx/yyy 提取 xxx/yyy
"""
marker = "$PSRCDIR/"
if marker not in path_str:
raise ValueError(f"路径中不包含 $PSRCDIR: {path_str}")
return path_str.split(marker, 1)[1].replace("\\", "/")
def find_file_by_tail(root_dir, tail_path):
"""
在 root_dir 下递归搜索,
找到唯一一个以 tail_path 结尾的文件
"""
matches = []
tail_path = tail_path.replace("/", os.sep)
for dirpath, _, filenames in os.walk(root_dir):
for name in filenames:
full_path = os.path.join(dirpath, name)
if full_path.endswith(tail_path):
matches.append(full_path)
if not matches:
# 如果没找到文件,返回 None 表示未找到
return None
if len(matches) > 1:
# 如果找到多个匹配,返回第一个,但打印警告
print(f"警告: 发现多个匹配文件,将使用第一个:\n" + "\n".join(matches))
return matches[0]
return matches[0]
def resolve_xpr_path(path_str, project_root, xpr_file_path):
"""
将 xpr 中的路径解析为真实绝对路径
支持 $PSRCDIR/ 和 $PPRDIR/ 两种 Vivado 路径变量
"""
# 处理 $PPRDIR/ 路径(项目根目录下的相对路径)
if "$PPRDIR/" in path_str:
tail = path_str.split("$PPRDIR/", 1)[1].replace("\\", "/")
direct_path = os.path.normpath(os.path.join(project_root, tail))
if os.path.exists(direct_path):
return direct_path
return find_file_by_tail(project_root, tail)
# 处理 $PSRCDIR/ 路径(原有逻辑不变)
if "$PSRCDIR/" not in path_str:
return None
tail = extract_tail_from_psrcdir(path_str)
xpr_filename = os.path.splitext(os.path.basename(xpr_file_path))[0]
srcs_dir = os.path.join(project_root, xpr_filename + ".srcs")
if os.path.isdir(srcs_dir):
real_path = find_file_by_tail(srcs_dir, tail)
if real_path is not None:
return real_path
return find_file_by_tail(project_root, tail)
# =================================================
# 通用 XML 工具
# =================================================
def _get_attr_local(element, local_name):
"""
忽略命名空间,按属性本地名获取值
(同时兼容带命名空间和不带命名空间的 XML
"""
for key, value in element.attrib.items():
if key == local_name or key.endswith("}" + local_name):
return value
return None
def _get_tag_local(element):
"""
忽略命名空间,返回标签的本地名
"""
tag = element.tag
return tag.split("}")[-1] if "}" in tag else tag
# =================================================
# 共享复制逻辑
# =================================================
def _copy_extracted_files(result_files, tb_files, failed_files):
"""
将解析出的文件列表复制到输出目录(公共逻辑)
"""
# 创建输出目录
out_dir = datetime.now().strftime("Copy_Files_%Y%m%d_%H%M%S")
os.makedirs(out_dir)
print(f"创建输出目录: {out_dir}")
tb_out_dir = None
if tb_files:
tb_out_dir = os.path.join(out_dir, "tb文件")
os.makedirs(tb_out_dir)
print(f"创建testbench输出目录: {tb_out_dir}")
copied_normal = 0
skipped_normal = 0
for src in result_files:
if not os.path.exists(src):
print(f"文件不存在,跳过: {src}")
skipped_normal += 1
continue
base = os.path.basename(src)
dst = os.path.join(out_dir, base)
if os.path.exists(dst):
name, ext = os.path.splitext(base)
idx = 1
while True:
new_dst = os.path.join(out_dir, f"{name}_{idx}{ext}")
if not os.path.exists(new_dst):
dst = new_dst
break
idx += 1
shutil.copy2(src, dst)
copied_normal += 1
copied_tb = 0
skipped_tb = 0
for src in tb_files:
if not os.path.exists(src):
print(f"testbench文件不存在跳过: {src}")
skipped_tb += 1
continue
base = os.path.basename(src)
dst = os.path.join(tb_out_dir, base) if tb_out_dir else os.path.join(out_dir, base)
if os.path.exists(dst):
name, ext = os.path.splitext(base)
idx = 1
while True:
new_dst = os.path.join(tb_out_dir, f"{name}_{idx}{ext}") if tb_out_dir else os.path.join(out_dir, f"{name}_{idx}{ext}")
if not os.path.exists(new_dst):
dst = new_dst
break
idx += 1
shutil.copy2(src, dst)
copied_tb += 1
print("复制完成")
print(f" 非testbench文件:")
print(f" 成功复制: {copied_normal}")
print(f" 跳过文件: {skipped_normal}")
print(f" testbench文件:")
print(f" 成功复制: {copied_tb}")
print(f" 跳过文件: {skipped_tb}")
print(f" 未找到文件: {len(failed_files)}")
if failed_files:
print(" 未找到的文件列表:")
for failed_file in failed_files:
print(f" - {failed_file}")
print(f" 输出目录: {os.path.abspath(out_dir)}")
if tb_files:
print("\n")
print("识别到项目中存在testbench文件,请在vivado中检查是否为源代码")
print("")
# =================================================
# 核心逻辑
# =================================================
def extract_from_xpr_and_copy(xpr_path, config_path, project_root):
"""
从 xpr 中解析 DesignSrcs + synthesis 文件集合,
使用模糊匹配解析真实路径并复制
"""
include_exts = load_extensions(config_path)
tree = ET.parse(xpr_path)
root = tree.getroot()
result_files = []
tb_files = []
failed_files = [] # 记录未找到的文件
# 只遍历 DesignSrcs
for fileset in root.iter("FileSet"):
if fileset.get("Type") != "DesignSrcs":
continue
for file_elem in fileset.findall("File"):
file_path = file_elem.get("Path")
if not file_path:
continue
# config.txt 后缀过滤 - 排除不需要的文件
suffix = Path(file_path).suffix.lower()
if include_exts and suffix not in include_exts:
continue
# testbench 命名判断
is_tb = is_testbench_filename(file_path)
fileinfo = file_elem.find("FileInfo")
if fileinfo is None:
continue
used_in_synth = False
auto_disabled = False
user_disabled = False
for attr in fileinfo.findall("Attr"):
name = attr.get("Name")
val = (attr.get("Val") or "").lower()
if name == "UsedIn" and val == "synthesis":
used_in_synth = True
elif name == "AutoDisabled" and val in ("1", "true"):
auto_disabled = True
elif name == "UserDisabled" and val in ("1", "true"):
user_disabled = True
if used_in_synth and not auto_disabled and not user_disabled:
real_path = resolve_xpr_path(file_path, project_root, xpr_path)
if real_path is not None:
if is_tb:
tb_files.append(real_path)
else:
result_files.append(real_path)
else:
failed_files.append(file_path) # 记录未找到的文件路径
print(f"解析并定位到 {len(result_files)} 个非testbench文件")
print(f"解析并定位到 {len(tb_files)} 个testbench文件")
if failed_files:
print(f"未能找到 {len(failed_files)} 个文件")
_copy_extracted_files(result_files, tb_files, failed_files)
# =================================================
# ISE (.xise) 解析
# =================================================
def extract_from_xise_and_copy(xise_path, config_path, project_root):
"""
从 .xise 中解析参与 Implementation 的源文件,
如果存在 .syr 文件,取 (.xise ∩ .syr) 交集,只保留同时出现在两个列表中的文件
"""
include_exts = load_extensions(config_path)
tree = ET.parse(xise_path)
root = tree.getroot()
# xise 解析结果:存原始路径(可能是相对也可能是绝对) + 解析后的绝对路径
xise_result = [] # 原始路径(来自 xise 文件)
xise_abs_map = {} # 原始路径 → 解析后的绝对路径(用于复制)
xise_tb = []
xise_tb_abs = {}
xise_library = [] # 带 <library/> 标签的文件,始终保留
xise_library_abs = {}
xise_failed = []
for file_elem in root.iter():
if _get_tag_local(file_elem) != "file":
continue
file_path = _get_attr_local(file_elem, "name")
if not file_path:
continue
suffix = Path(file_path).suffix.lower()
if include_exts and suffix not in include_exts:
continue
is_tb = is_testbench_filename(file_path)
has_impl = False
has_library = False
for elem in file_elem.iter():
tag = _get_tag_local(elem)
if tag == "association":
if _get_attr_local(elem, "name") == "Implementation":
has_impl = True
elif tag == "library":
has_library = True
if not has_impl:
continue
raw_path = file_path.replace("\\", "/")
# 尝试用 project_root 拼接解析(相对路径和绝对路径都能处理)
real_path = os.path.normpath(os.path.join(project_root, raw_path))
if not os.path.exists(real_path):
# 可能是从其他电脑拷过来的绝对路径,用文件名在 project_root 下搜索
real_path = find_file_by_tail(project_root, os.path.basename(raw_path))
if real_path and os.path.exists(real_path):
if is_tb:
xise_tb.append(raw_path)
xise_tb_abs[raw_path] = real_path
else:
xise_result.append(raw_path)
xise_abs_map[raw_path] = real_path
if has_library:
xise_library.append(raw_path)
xise_library_abs[raw_path] = real_path
else:
xise_failed.append(raw_path)
print(f"从 .xise 解析到 {len(xise_result)} 个非testbench文件")
print(f"从 .xise 解析到 {len(xise_tb)} 个testbench文件")
if xise_library:
print(f"其中 {len(xise_library)} 个带 <library/> 标签,将始终保留")
# 查找 .syr 文件,如果有则提取并取交集
syr_files = find_all_files_in_dir(project_root, ".syr")
if syr_files:
print(f"在项目根目录找到 {len(syr_files)} 个 .syr 文件")
chosen_syr = user_select_file(syr_files, ".syr")
print(f"使用 syr 文件进行二次过滤: {chosen_syr}")
syr_result, syr_tb, syr_failed = _parse_syr(chosen_syr, config_path, project_root)
result_files = [] # 交集结果xise 原始路径
tb_files = []
# 第一阶段:严格匹配 —— 双方解析后的绝对路径做 normcase 比较
syr_all_norm = set(os.path.normcase(f) for f in syr_result + syr_tb)
for raw_path in xise_result:
abs_path = xise_abs_map.get(raw_path)
if abs_path and os.path.normcase(abs_path) in syr_all_norm:
result_files.append(raw_path)
for raw_path in xise_tb:
abs_path = xise_tb_abs.get(raw_path)
if abs_path and os.path.normcase(abs_path) in syr_all_norm:
tb_files.append(raw_path)
# 第二阶段:尾部组件匹配(补配因绝对/相对路径差异导致未匹配的文件)
# 注意syr_all 必须包含 syr_failed 中的原始路径 ——
# syr 里来自其他电脑的绝对路径 resolve 失败后进了 syr_failed
# 但它们的尾部仍然能和 xise 相对路径匹配上,必须参与比较
xise_remaining = [f for f in xise_result if f not in result_files]
xise_tb_remaining = [f for f in xise_tb if f not in tb_files]
if xise_remaining or xise_tb_remaining:
syr_all = syr_result + syr_tb + syr_failed
tail_matched = []
matched_syr_failed = set()
for raw_path in xise_remaining:
for syr_path in syr_all:
if paths_match_tail(raw_path, syr_path):
result_files.append(raw_path)
tail_matched.append(raw_path)
if syr_path in syr_failed:
matched_syr_failed.add(syr_path)
break
for raw_path in xise_tb_remaining:
for syr_path in syr_all:
if paths_match_tail(raw_path, syr_path):
tb_files.append(raw_path)
tail_matched.append(raw_path)
if syr_path in syr_failed:
matched_syr_failed.add(syr_path)
break
if tail_matched:
print(f"尾部匹配补配 {len(tail_matched)} 个文件(绝对/相对路径差异)")
# 已匹配的 syr 路径从失败列表中移除,避免误导
failed_files = xise_failed + [f for f in syr_failed if f not in matched_syr_failed]
print(f"\n交集后保留 {len(result_files)} 个非testbench文件")
print(f"交集后保留 {len(tb_files)} 个testbench文件")
else:
result_files = xise_result
tb_files = xise_tb
failed_files = xise_failed
print("项目根目录未找到 .syr 文件,直接使用 .xise 结果")
if failed_files:
print(f"未能找到 {len(failed_files)} 个文件")
# 用 xise 侧映射重建绝对路径用于复制xise/syr 中的绝对路径可能来自其他电脑,不可信)
def _resolve_xise(raw_path, abs_map):
if raw_path in abs_map:
return abs_map[raw_path]
# 兜底:直接用 project_root 拼接
return os.path.normpath(os.path.join(project_root, raw_path.replace("\\", "/")))
result_files_abs = [_resolve_xise(f, xise_abs_map) for f in result_files]
tb_files_abs = [_resolve_xise(f, xise_tb_abs) for f in tb_files]
# 合并带 <library/> 标签的文件(始终保留)
for raw_path in xise_library:
abs_path = _resolve_xise(raw_path, xise_library_abs)
if abs_path not in result_files_abs and abs_path not in tb_files_abs:
result_files_abs.append(abs_path)
_copy_extracted_files(result_files_abs, tb_files_abs, failed_files)
# =================================================
# ISE (.syr) 解析
# =================================================
SYR_PATTERN = re.compile(r'Compiling\s+\w+\s+file\s+"([^"]+)"', re.IGNORECASE)
def paths_match_tail(path_a, path_b):
"""
判断两个路径的尾部组件是否匹配。
将路径拆分为组件列表,检查较短列表是否为较长列表的后缀。
无论绝对/相对路径、正斜杠/反斜杠、盘符差异,都能正确匹配。
例: ../src/pro/xxx.v vs C:/Users/.../src/pro/xxx.v → 匹配(尾部 src/pro/xxx.v 相同)
./xxx.v vs F:/project/xxx.v → 匹配
xxx.v vs F:/project/xxx.v → 匹配
"""
def _normalize_parts(p):
p = p.replace("\\", "/").lower()
if len(p) >= 2 and p[1] == ":":
p = p[2:]
while p.startswith("../"):
p = p[3:]
while p.startswith("./"):
p = p[2:]
p = p.strip("/")
return p.split("/") if p else []
parts_a = _normalize_parts(path_a)
parts_b = _normalize_parts(path_b)
if not parts_a or not parts_b:
return False
if len(parts_a) <= len(parts_b):
return parts_a == parts_b[-len(parts_a):]
else:
return parts_b == parts_a[-len(parts_b):]
def resolve_syr_path(extracted_path, project_root):
"""
从 .syr 中提取的路径解析为真实绝对路径
支持绝对路径 / 相对路径 / 纯文件名
"""
extracted_path = extracted_path.replace("\\", "/").strip()
if os.path.isabs(extracted_path):
if os.path.exists(extracted_path):
return os.path.normpath(extracted_path)
filename = os.path.basename(extracted_path)
if os.path.exists(os.path.join(project_root, filename)):
return os.path.normpath(os.path.join(project_root, filename))
# 相对路径:和 xise 一样用 os.path.join + normpath保留完整目录结构
real_path = os.path.normpath(os.path.join(project_root, extracted_path))
if os.path.exists(real_path):
return real_path
# 回退:用文件名模糊搜索
tail = os.path.basename(extracted_path)
return find_file_by_tail(project_root, tail)
def _parse_syr(syr_path, config_path, project_root):
"""
从 .syr 综合报告中解析源文件,返回 (result_files, tb_files, failed_files)
不执行复制,供内部调用做交集等操作
"""
include_exts = load_extensions(config_path)
result_files = []
tb_files = []
failed_files = []
seen_paths = set()
with open(syr_path, "r", encoding="utf-8", errors="ignore") as f:
for line in f:
matches = SYR_PATTERN.findall(line)
for extracted_path in matches:
if extracted_path in seen_paths:
continue
seen_paths.add(extracted_path)
suffix = Path(extracted_path).suffix.lower()
if include_exts and suffix not in include_exts:
continue
is_tb = is_testbench_filename(extracted_path)
real_path = resolve_syr_path(extracted_path, project_root)
if real_path is not None and os.path.exists(real_path):
if is_tb:
tb_files.append(real_path)
else:
result_files.append(real_path)
else:
failed_files.append(extracted_path)
return result_files, tb_files, failed_files
def extract_from_syr_and_copy(syr_path, config_path, project_root):
"""
从 .syr 综合报告中解析源文件并复制
匹配模式: Compiling vhdl file "path/to/file.vhd" in Library work
"""
result_files, tb_files, failed_files = _parse_syr(syr_path, config_path, project_root)
print(f"解析并定位到 {len(result_files)} 个非testbench文件")
print(f"解析并定位到 {len(tb_files)} 个testbench文件")
if failed_files:
print(f"未能找到 {len(failed_files)} 个文件")
_copy_extracted_files(result_files, tb_files, failed_files)
# =================================================
# main
# =================================================
if __name__ == "__main__":
try:
PROJECT_ROOT = get_project_root()
if not os.path.isdir(PROJECT_ROOT):
raise RuntimeError(f"PROJECT_ROOT 不存在: {PROJECT_ROOT}")
config_file = os.path.join(os.getcwd(), "config.txt")
if not os.path.exists(config_file):
raise FileNotFoundError("当前目录下未找到 config.txt")
print(f"使用 config 文件: {config_file}")
print(f"项目根目录 : {PROJECT_ROOT}")
# 优先级: .xpr → .xise → .syr
xpr_file = find_latest_xpr_in_dir(PROJECT_ROOT)
if xpr_file:
print(f"使用 xpr 文件 : {xpr_file}")
extract_from_xpr_and_copy(xpr_file, config_file, PROJECT_ROOT)
else:
xise_files = find_all_files_in_dir(PROJECT_ROOT, ".xise")
if xise_files:
chosen_xise = user_select_file(xise_files, ".xise")
print(f"使用 xise 文件 : {chosen_xise}")
extract_from_xise_and_copy(chosen_xise, config_file, PROJECT_ROOT)
else:
syr_files = find_all_files_in_dir(PROJECT_ROOT, ".syr")
if syr_files:
chosen_syr = user_select_file(syr_files, ".syr")
print(f"使用 syr 文件 : {chosen_syr}")
extract_from_syr_and_copy(chosen_syr, config_file, PROJECT_ROOT)
else:
print("当前项目下没有能提取源代码的文件依据")
except Exception as e:
print("程序执行失败:")
print(e)
finally:
input("\n按回车键关闭窗口...")