#!/usr/bin/env python3 """根据 requirements.txt 生成 requirements.lock.txt(精确版本)。 读取 requirements.txt 列出的直接依赖名,从当前已安装环境查出精确版本, 写出 requirements.lock.txt。在「已验证可用」的环境中运行,确保锁定的是 一组实测能跑起来的版本。 用法: python -m scripts.gen_requirements_lock python scripts/gen_requirements_lock.py """ from __future__ import annotations import importlib.metadata as md import re import sys from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] REQ_PATH = REPO_ROOT / "requirements.txt" LOCK_PATH = REPO_ROOT / "requirements.lock.txt" _LOCK_HEADER = """\ # 锁定版本依赖(构建 / release 打包用)。由 scripts/gen_requirements_lock.py 生成。 # # 与 requirements.txt 的关系: # - requirements.txt 人读、宽松,列出运行时直接依赖(不锁版本) # - requirements.lock.txt(本文件) 精确版本,供便携 release 打包与可复现安装 # # 安装:pip install -r requirements.lock.txt # 更新:在已验证可用的环境执行 python -m scripts.gen_requirements_lock # # 注:这里只锁项目运行时直接依赖;其传递依赖由 pip 在安装时解析。 """ def _parse_requirement_names(text: str) -> list[str]: """从 requirements.txt 提取包名(忽略注释、空行、已带版本约束的尾巴)。""" names: list[str] = [] for raw in text.splitlines(): line = raw.strip() if not line or line.startswith("#"): continue # 去掉版本约束/extras,仅留包名: flask[async]>=2 -> flask name = re.split(r"[<>=!~\[ ;]", line, 1)[0].strip() if name: names.append(name) return names def main() -> int: if not REQ_PATH.exists(): print(f"找不到 {REQ_PATH}", file=sys.stderr) return 1 names = _parse_requirement_names(REQ_PATH.read_text(encoding="utf-8")) if not names: print("requirements.txt 中没有可锁定的依赖。", file=sys.stderr) return 1 lines: list[str] = [] missing: list[str] = [] for name in names: try: version = md.version(name) lines.append(f"{name}=={version}") except md.PackageNotFoundError: missing.append(name) lines.append(f"# {name} (未安装,无法锁定版本)") LOCK_PATH.write_text(_LOCK_HEADER + "\n" + "\n".join(lines) + "\n", encoding="utf-8") print(f"已写出 {LOCK_PATH}({len(names)} 项依赖)") for line in lines: print(" " + line) if missing: print(f"\n警告:以下依赖未安装,未能锁定版本:{', '.join(missing)}", file=sys.stderr) print("请在已安装全部依赖的环境中重新运行。", file=sys.stderr) return 2 return 0 if __name__ == "__main__": sys.exit(main())