#!/usr/bin/env python # -*- coding: utf-8 -*- """门禁钩子的统一启动器:替钩子找到该用的解释器,再原样转交脚本。 被提交的 .pre-commit-config.yaml 只写 `entry: scripts/gate_python.py <脚本> [参数...]`, 不出现任何机器相关的解释器路径——换机、换克隆目录、换 PATH 首位都不必改配置。 解释器按下面的顺序找,第一个可用的胜出: 1. $DTU_GATE_PYTHON 显式覆盖(临时切版本、排查「到底跑在谁身上」) 2. <仓库>/.venv/Scripts/python.exe 约定的仓库内环境(requirements.txt 装在这里) 3. <仓库>/.gate/python/python.exe 便携解释器(可选,本仓当前未提供) 4. PATH 上的 python 最后兜底 约定纪律(改动前先读 docs/环境与部署/本地工具路径环境变量方案.md §5.1): - 纯标准库、单文件、不 import 仓库内任何模块——它运行在「解释器还不知道是谁」的时刻; - 解析出的解释器用环境变量 DTU_GATE_PYTHON 传给子进程,脚本内部再调 Python 时能与钩子保持一致, 否则「门禁用的版本」和「生成器用的版本」会分叉。 退出码:0 表示被调脚本自己退出为 0;启动器自身失败一律 2(与 pre-commit「环境不可用」口径一致, 见 docs/草稿约定:2 = 配置/环境问题,1 = 检查结论不通过)。 """ from __future__ import annotations import os import sys LAUNCHER_NAME = "gate_python.py" def repo_root() -> str: """启动器所在仓库根(本文件位于 <仓库>/scripts/ 下)。""" return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) def candidates(root: str) -> list[tuple[str, str]]: """返回 [(说明, 解释器路径)],顺序即优先级。""" items: list[tuple[str, str]] = [] override = os.environ.get("DTU_GATE_PYTHON") if override: items.append(("$DTU_GATE_PYTHON", override)) items.append(("仓库 .venv", os.path.join(root, ".venv", "Scripts", "python.exe"))) items.append(("便携解释器 .gate", os.path.join(root, ".gate", "python", "python.exe"))) path_python = path_lookup("python") if path_python: items.append(("PATH", path_python)) return items def path_lookup(name: str) -> str: """按 Windows 语义在 PATH 里找可执行文件(含 PATHEXT 展开)。""" exts = os.environ.get("PATHEXT", ".COM;.EXE;.BAT;.CMD").split(os.pathsep) for directory in os.environ.get("PATH", "").split(os.pathsep): if not directory: continue for ext in [""] + exts: candidate = os.path.join(directory, name + ext.lower()) if os.path.isfile(candidate): return candidate return "" def pick(root: str) -> tuple[str, str] | None: for label, exe in candidates(root): if exe and os.path.isfile(exe): return label, exe return None def fail(message: str, root: str) -> int: print(f"[gate_python] {message}", file=sys.stderr) print("[gate_python] 解释器候选检查结果:", file=sys.stderr) for label, exe in candidates(root): state = "可用" if exe and os.path.isfile(exe) else "不存在" print(f" {label:<16} {exe or '<未设置>'} [{state}]", file=sys.stderr) print( "[gate_python] 修复:按下面任一方式让第 2 个候选就位——\n" " python -m venv .venv && .venv/Scripts/python.exe -m pip install -r requirements.txt\n" " 或临时指定:set DTU_GATE_PYTHON=<某个装了依赖的 python.exe>\n" " 详细规程 → docs/环境与部署/本地工具路径环境变量方案.md §5", file=sys.stderr, ) return 2 def main(argv: list[str]) -> int: root = repo_root() if len(argv) < 2: return fail("缺少目标脚本参数(用法:gate_python.py <脚本> [参数...])", root) target = argv[1] if not os.path.isabs(target): target = os.path.join(root, target) if not os.path.isfile(target): return fail(f"目标脚本不存在:{argv[1]}", root) chosen = pick(root) if chosen is None: return fail("找不到可用的解释器", root) label, exe = chosen if os.environ.get("DTU_GATE_VERBOSE"): print(f"[gate_python] {label} -> {exe}", file=sys.stderr) env = dict(os.environ) env["DTU_GATE_PYTHON"] = exe child_argv = [exe, target, *argv[2:]] os.execve(exe, child_argv, env) return 0 # 不会走到这里:execve 成功即替换进程 if __name__ == "__main__": sys.exit(main(sys.argv))