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 = [] # 带 标签的文件,始终保留 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)} 个带 标签,将始终保留") # 查找 .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] # 合并带 标签的文件(始终保留) 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按回车键关闭窗口...")