Files
STM32F4-Base/scripts/gate_python.py
T
wandering1 96a3d15efc 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 全通过。
2026-11-15 19:39:56 +08:00

114 lines
4.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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))