chore(gate): 门禁钩子改走 gate_python 启动器,worktree 统一插件模型
门禁解释器解耦
44 条 language: system 钩子此前把解释器写成仓库检出的绝对路径,换机、
换克隆目录都要重写。改为 entry 只写 scripts/gate_python.py + 脚本名,
由启动器按候选链解析:$DTU_GATE_PYTHON > <仓库>/.venv >
<仓库>/.gate/python > PATH。启动器用 execve 替换自身,并把选中的
解释器经 DTU_GATE_PYTHON 传给子进程,保证脚本内再调 Python 时一致。
实测 pre-commit 4.6.2 不展开 ${VAR};language: script 的 shebang 只做
纯 PATH 改写,会把决定权交给 PATH 首个 python,故不采用。
ci_local.py 预检识别启动器并报出解析结果。
并行工作树统一
task-worktree 新增 bench-gate(强制台架验收)与 merge(编排式合并),
删除 new/abort,done 标 DEPRECATED;台架通过记录改存
.agents/worktrees/.bench-pass-log。keil_build.py 的 plugin_tree_owner
识别 .agents/worktrees/<name> 并返回 pw:<name>。
check_doc_links / gen_refgraph / check_layer_deps 的扫描排除面补上
.agents;并行任务工作树方案重写为单一模型(7 节),取消工作树数量上限,
删历史实测记录节,两处 §7.3 悬空引用改为按语义引用。
文档与约定
本地工具路径环境变量方案 §5 重写为四节:谁跑在哪个 Python 上、写新钩子、
预检对号入座、动环境五步。文档规范的变更记录例外名单由 5 篇减为 4 篇
(本篇实际没有该节)。AGENTS.md 补委派验收口径三条。
验证:pre_commit run --all-files 48 钩子全 Passed;ci_local.py
--preflight-only 报 44 条经启动器 -> 仓库 .venv;keil-build 0 errors
0 warnings;check_doc_links --sections-strict 全通过。
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
#!/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))
|
||||
Reference in New Issue
Block a user