Files

713 lines
25 KiB
Python
Raw Permalink Normal View History

2026-07-13 17:55:17 +08:00
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按回车键关闭窗口...")