fix(repo): 修正 Scripts 目录索引大小写并补齐未入库脚本
问题 - 索引中该目录记为小写 `scripts/`,工作区实为 `Scripts/`。因 core.ignorecase=true, 本地 `git diff -- Scripts/bin/x.py` 会**静默返回空**(看似无改动), 且在大小写敏感环境(Linux CI / Docker 挂载)会检出 `scripts/`, 与全仓所有 `Scripts/...` 引用不匹配。 - 该目录此前 10 个脚本(check_python_version.py、perf_snapshot.py、 install_collector_launchd.py、mysql_toggle.sh 等)从未入库, 而 CI 第 31 行正调用 `Scripts/bin/check_python_version.py --strict` —— 全新检出必然失败。 改动 - 索引路径统一为 `Scripts/`(工作区文件未移动,仅索引改名)。 - 补齐上述 10 个未跟踪脚本及其测试。 影响 - 修正前索引 35 条(全为小写路径),修正后 45 条。
This commit is contained in:
1 parent
2e019cd118
commit
11d36d1c1e
46 files changed
+3089
-149
No files matched your search
Executable
+253
@@ -0,0 +1,253 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Python 版本一致性检查 CLI
|
||||
==========================
|
||||
|
||||
FinDataHub 全项目统一锁定 Python 3.12(见仓库根目录 .python-version)。
|
||||
本脚本校验「声明」与「实际」是否都已对齐,供本地自查与 CI 使用。
|
||||
|
||||
检查项
|
||||
------
|
||||
1. 所有 pyproject.toml 的 requires-python 是否接受目标版本
|
||||
2. 各服务 .venv 的实际 Python 版本是否为目标版本
|
||||
3. 代码里是否残留硬编码的其它 Python 版本(python3.13 / python@3.13 等)
|
||||
4. Scripts/bin 下脚本的 shebang 是否指向目标版本(或为环境无关的 env 形式)
|
||||
|
||||
用法
|
||||
----
|
||||
python3 check_python_version.py # 人类可读报告
|
||||
python3 check_python_version.py --json # JSON 输出
|
||||
python3 check_python_version.py --strict # 警告也视为失败(CI 用)
|
||||
|
||||
退出码
|
||||
------
|
||||
0 全部通过(--strict 下警告也算通过,只有 error 才失败)
|
||||
1 存在 error
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 仓库根目录:本脚本位于 <root>/Scripts/bin/
|
||||
ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
VERSION_FILE = ROOT / ".python-version"
|
||||
|
||||
# 参与版本检查的服务(相对 ROOT)
|
||||
SERVICES = [
|
||||
"Services/akshare_fetcher",
|
||||
"Services/tushare_fetcher",
|
||||
"Services/kuaixun-service",
|
||||
"Services/findatahub-core",
|
||||
"Services/findatahub-client",
|
||||
]
|
||||
|
||||
# 需要扫描硬编码版本引用的目录与文件后缀
|
||||
SCAN_DIRS = ["Scripts", "App/Sources", "Services"]
|
||||
SCAN_SUFFIX = {".py", ".swift", ".sh", ".toml", ".yml", ".yaml"}
|
||||
# 这些文件是历史记录/报告,不参与检查
|
||||
SCAN_EXCLUDE = {
|
||||
"DEVLOG.md",
|
||||
"FinDataHub-性能与资源分析.html",
|
||||
"FinDataHub-能耗优化分析.html",
|
||||
}
|
||||
|
||||
|
||||
def read_target_version():
|
||||
if not VERSION_FILE.exists():
|
||||
return None, f"缺少 {VERSION_FILE}"
|
||||
v = VERSION_FILE.read_text(encoding="utf-8").strip()
|
||||
if not re.fullmatch(r"3\.\d+", v):
|
||||
return None, f"{VERSION_FILE} 内容不合法:{v!r}(应为形如 3.12)"
|
||||
return v, None
|
||||
|
||||
|
||||
def parse_requires_python(text):
|
||||
"""从 pyproject.toml 文本里取出 requires-python 字符串"""
|
||||
m = re.search(r'^requires-python\s*=\s*["\']([^"\']+)["\']', text, re.MULTILINE)
|
||||
return m.group(1) if m else None
|
||||
|
||||
|
||||
def version_accepts(spec, target):
|
||||
"""
|
||||
判断 requires-python 说明符是否接受目标版本。
|
||||
支持形如:>=3.12、>=3.12,<3.13、>=3.10、==3.12.*
|
||||
"""
|
||||
def key(s):
|
||||
return tuple(int(x) for x in s.split("."))
|
||||
|
||||
t = key(target)
|
||||
ok = True
|
||||
for clause in [c.strip() for c in spec.split(",") if c.strip()]:
|
||||
m = re.fullmatch(r"(==|>=|<=|>|<|!=|~=)\s*([0-9.*]+)", clause)
|
||||
if not m:
|
||||
continue # 无法解析的子句跳过,不误报
|
||||
op, ver = m.group(1), m.group(2)
|
||||
if "*" in ver: # ==3.12.*
|
||||
base = key(ver.replace(".*", "").rstrip("."))
|
||||
ok = ok and (op == "==" and t[:len(base)] == base) or (op == "!=" and t[:len(base)] != base)
|
||||
continue
|
||||
v = key(ver)
|
||||
if op == ">=":
|
||||
ok = ok and t >= v
|
||||
elif op == "<=":
|
||||
ok = ok and t <= v
|
||||
elif op == ">":
|
||||
ok = ok and t > v
|
||||
elif op == "<":
|
||||
ok = ok and t < v
|
||||
elif op == "==":
|
||||
ok = ok and t == v
|
||||
elif op == "!=":
|
||||
ok = ok and t != v
|
||||
return ok
|
||||
|
||||
|
||||
def check_pyprojects(target):
|
||||
issues = []
|
||||
for svc in SERVICES:
|
||||
p = ROOT / svc / "pyproject.toml"
|
||||
if not p.exists():
|
||||
issues.append(("warn", f"{svc}: 缺少 pyproject.toml"))
|
||||
continue
|
||||
spec = parse_requires_python(p.read_text(encoding="utf-8"))
|
||||
if spec is None:
|
||||
issues.append(("warn", f"{svc}: 未声明 requires-python"))
|
||||
continue
|
||||
if not version_accepts(spec, target):
|
||||
issues.append(("error", f"{svc}: requires-python = {spec!r} 不接受 {target}"))
|
||||
return issues
|
||||
|
||||
|
||||
def check_venvs(target):
|
||||
issues = []
|
||||
for svc in SERVICES:
|
||||
venv = ROOT / svc / ".venv"
|
||||
if not venv.exists():
|
||||
issues.append(("warn", f"{svc}: 无 .venv(未初始化)"))
|
||||
continue
|
||||
cfg = venv / "pyvenv.cfg"
|
||||
actual = None
|
||||
if cfg.exists():
|
||||
txt = cfg.read_text(encoding="utf-8")
|
||||
m = re.search(r"^version(?:_info)?\s*=\s*([0-9.]+)", txt, re.MULTILINE)
|
||||
if m:
|
||||
actual = m.group(1)
|
||||
if actual is None:
|
||||
# 回退:从 lib/python3.X 目录推断
|
||||
lib = venv / "lib"
|
||||
vers = sorted(d.name.replace("python", "")
|
||||
for d in lib.iterdir()
|
||||
if d.name.startswith("python") and d.is_dir()) if lib.exists() else []
|
||||
actual = vers[-1] if vers else None
|
||||
if actual is None:
|
||||
issues.append(("warn", f"{svc}: 无法判定 venv 版本"))
|
||||
elif not actual.startswith(target):
|
||||
issues.append(("error", f"{svc}: venv 实际版本 {actual},目标 {target}"))
|
||||
return issues
|
||||
|
||||
|
||||
def check_hardcoded(target):
|
||||
"""
|
||||
扫描代码中残留的其它 python3.X 硬编码引用。
|
||||
|
||||
只匹配 `python3.XX` / `python@3.XX` 两种形式(不含 ruff 的 `py312`——
|
||||
那是 linter 配置,不是版本引用)。注释行、本脚本自身、以及测试里
|
||||
刻意构造的「旧版本残留」数据均不计入。
|
||||
"""
|
||||
issues = []
|
||||
pat = re.compile(r"python3\.(\d+)|python@3\.(\d+)")
|
||||
self_name = Path(__file__).name
|
||||
|
||||
for d in SCAN_DIRS:
|
||||
base = ROOT / d
|
||||
if not base.exists():
|
||||
continue
|
||||
for f in base.rglob("*"):
|
||||
if not f.is_file() or f.suffix not in SCAN_SUFFIX:
|
||||
continue
|
||||
if any(x in str(f) for x in (".venv", "node_modules", "__pycache__", ".build")):
|
||||
continue
|
||||
if f.name in SCAN_EXCLUDE or f.name == self_name:
|
||||
continue
|
||||
try:
|
||||
txt = f.read_text(encoding="utf-8", errors="ignore")
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
for i, raw in enumerate(txt.splitlines(), 1):
|
||||
line = raw.strip()
|
||||
# 跳过注释(Swift / Python / Shell / YAML 通用)
|
||||
if line.startswith(("#", "//", "///", "*")):
|
||||
continue
|
||||
# 跳过动态取版本的行
|
||||
if "PYTHON_TARGET_VERSION" in line or "findPython" in line:
|
||||
continue
|
||||
# 测试文件里刻意构造的旧版本数据不算问题
|
||||
if "tests/" in str(f).replace("\\", "/") and "tmp_path" in txt:
|
||||
continue
|
||||
|
||||
for m in pat.finditer(line):
|
||||
found = f"3.{m.group(1) or m.group(2)}"
|
||||
if found != target:
|
||||
rel = f.relative_to(ROOT)
|
||||
issues.append((
|
||||
"warn",
|
||||
f"{rel}:{i} 硬编码 python {found}(目标 {target})— {line[:70]}"
|
||||
))
|
||||
break
|
||||
return issues
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="检查全项目 Python 版本一致性")
|
||||
ap.add_argument("--json", action="store_true", help="JSON 输出")
|
||||
ap.add_argument("--strict", action="store_true", help="警告也计入失败")
|
||||
args = ap.parse_args()
|
||||
|
||||
target, err = read_target_version()
|
||||
if err:
|
||||
print(f"error: {err}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
issues = []
|
||||
issues += check_pyprojects(target)
|
||||
issues += check_venvs(target)
|
||||
issues += check_hardcoded(target)
|
||||
|
||||
errors = [m for lv, m in issues if lv == "error"]
|
||||
warns = [m for lv, m in issues if lv == "warn"]
|
||||
|
||||
if args.json:
|
||||
print(json.dumps({
|
||||
"target_version": target,
|
||||
"errors": errors,
|
||||
"warnings": warns,
|
||||
"passed": not errors and (not args.strict or not warns),
|
||||
}, ensure_ascii=False, indent=2))
|
||||
else:
|
||||
print(f"目标版本:Python {target}(来源 {VERSION_FILE.relative_to(ROOT)})\n")
|
||||
if errors:
|
||||
print(f"✗ 错误 {len(errors)} 项:")
|
||||
for m in errors:
|
||||
print(f" {m}")
|
||||
if warns:
|
||||
print(f"⚠ 警告 {len(warns)} 项:")
|
||||
for m in warns:
|
||||
print(f" {m}")
|
||||
if not errors and not warns:
|
||||
print("✓ 全部检查通过:声明、venv、硬编码引用均已对齐。")
|
||||
elif not errors:
|
||||
print(f"\n✓ 无致命错误({len(warns)} 项警告)")
|
||||
|
||||
if errors:
|
||||
return 1
|
||||
if args.strict and warns:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user