forked from wxhwwla/calc-framework
322 lines
10 KiB
Python
322 lines
10 KiB
Python
#!/usr/bin/env python3
|
||
# SPDX-License-Identifier: AGPL-3.0
|
||
"""
|
||
项目版本号与上传元数据 — 版本信息唯一源头。
|
||
|
||
此文件是 `_VERSION` 和 `_EXE_VERSION` 的唯一定义处。
|
||
`scripts/please_read_me.py` / `upload_meta.py` / `version.py` 均从此模块导入。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from pathlib import Path
|
||
|
||
# ==================== 版本常量(唯一源头) ====================
|
||
|
||
_VERSION = "3.21.3"
|
||
"""项目与 pip 包版本(pyproject.toml 通过 dynamic 读取)。
|
||
|
||
上传脚本在有「业务改动」并 push 成功时自动递增(默认第三位 +1)。
|
||
第一位 MAJOR 永远只在下方手动修改,脚本不会动。"""
|
||
|
||
_EXE_VERSION = "0.6.0-beta"
|
||
"""窗口标题与 dist/*.exe 用户可见版本。
|
||
|
||
仅手动修改;改后须重新打包(main_build.py)。"""
|
||
|
||
# ==============================================================
|
||
|
||
_UPLOAD_SUMMARY_BEGIN = "# --- BEGIN UPLOAD_SUMMARY ---"
|
||
_UPLOAD_SUMMARY_END = "# --- END UPLOAD_SUMMARY ---"
|
||
SUMMARY_BEGIN = _UPLOAD_SUMMARY_BEGIN
|
||
SUMMARY_END = _UPLOAD_SUMMARY_END
|
||
_SUMMARY_ASSIGNMENTS_BLOCK = "SUMMARY_BEGIN = _UPLOAD_SUMMARY_BEGIN\nSUMMARY_END = _UPLOAD_SUMMARY_END\n"
|
||
_SUMMARY_ASSIGNMENTS_PATTERN = re.compile(
|
||
r"^SUMMARY_BEGIN\s*=.*?[\r\n]+(?:^SUMMARY_END\s*=.*?[\r\n]+)?",
|
||
re.MULTILINE,
|
||
)
|
||
|
||
_VERSION_PATTERN = re.compile(
|
||
r'^(_VERSION\s*=\s*["\'])([^"\']+)(["\'])',
|
||
re.MULTILINE,
|
||
)
|
||
|
||
_EXE_VERSION_PATTERN = re.compile(
|
||
r'^(_EXE_VERSION\s*=\s*["\'])([^"\']+)(["\'])',
|
||
re.MULTILINE,
|
||
)
|
||
|
||
__all__ = [
|
||
"SUMMARY_BEGIN",
|
||
"SUMMARY_END",
|
||
"_EXE_VERSION",
|
||
"_EXE_VERSION_PATTERN",
|
||
"_VERSION",
|
||
"_VERSION_PATTERN",
|
||
"build_commit_message",
|
||
"bump_minor",
|
||
"bump_patch",
|
||
"classify_changed_paths",
|
||
"ensure_summary_marker_assignments",
|
||
"format_semver",
|
||
"get_exe_version",
|
||
"get_version",
|
||
"parse_semver",
|
||
"please_read_me_path",
|
||
"read_exe_version",
|
||
"read_summary_for_commit",
|
||
"read_version",
|
||
"remove_summary_block",
|
||
"strip_summary_block",
|
||
"summarize_changes",
|
||
"write_summary_block",
|
||
"write_version",
|
||
]
|
||
|
||
|
||
def please_read_me_path(repo_root: Path | None = None) -> Path:
|
||
"""返回 _version.py 的路径(版本唯一源头)。
|
||
|
||
历史说明:旧名为 `please_read_me_path`,因版本常量原在
|
||
`please_read_me.py` 中。现常量已迁移到此文件,但函数名
|
||
保留以保持调用兼容。
|
||
"""
|
||
root = repo_root or Path(__file__).resolve().parent
|
||
return root / "_version.py"
|
||
|
||
|
||
def parse_semver(version: str) -> tuple[int, int, int]:
|
||
"""解析语义化版本字符串为三元组。"""
|
||
parts = version.strip().split(".")
|
||
if len(parts) != 3 or not all(p.isdigit() for p in parts):
|
||
raise ValueError(f"无效语义化版本: {version!r}(需要 MAJOR.MINOR.PATCH)")
|
||
return int(parts[0]), int(parts[1]), int(parts[2])
|
||
|
||
|
||
def format_semver(major: int, minor: int, patch: int) -> str:
|
||
"""将整数三元组格式化为语义化版本字符串。"""
|
||
return f"{major}.{minor}.{patch}"
|
||
|
||
|
||
def read_version(path: Path | None = None) -> str:
|
||
"""从 _version.py 读取 _VERSION。"""
|
||
path = path or please_read_me_path()
|
||
text = path.read_text(encoding="utf-8")
|
||
match = _VERSION_PATTERN.search(text)
|
||
if not match:
|
||
raise ValueError(f"未在 {path} 中找到 _VERSION")
|
||
return match.group(2)
|
||
|
||
|
||
def read_exe_version(path: Path | None = None) -> str:
|
||
"""从 _version.py 读取 _EXE_VERSION。"""
|
||
path = path or please_read_me_path()
|
||
text = path.read_text(encoding="utf-8")
|
||
match = _EXE_VERSION_PATTERN.search(text)
|
||
if not match:
|
||
raise ValueError(f"未在 {path} 中找到 _EXE_VERSION")
|
||
return match.group(2)
|
||
|
||
|
||
def write_version(path: Path | None, new_version: str) -> None:
|
||
"""将新版本号写入 _version.py 的 _VERSION。"""
|
||
path = path or please_read_me_path()
|
||
text = path.read_text(encoding="utf-8")
|
||
if not _VERSION_PATTERN.search(text):
|
||
raise ValueError(f"未在 {path} 中找到 _VERSION")
|
||
updated = _VERSION_PATTERN.sub(
|
||
rf"\g<1>{new_version}\g<3>",
|
||
text,
|
||
count=1,
|
||
)
|
||
path.write_text(updated, encoding="utf-8")
|
||
ensure_summary_marker_assignments(path)
|
||
|
||
|
||
def bump_patch(version: str) -> str:
|
||
"""版本号第三位 +1。"""
|
||
major, minor, patch = parse_semver(version)
|
||
return format_semver(major, minor, patch + 1)
|
||
|
||
|
||
def bump_minor(version: str) -> str:
|
||
"""版本号第二位 +1,第三位置零。"""
|
||
major, minor, _patch = parse_semver(version)
|
||
return format_semver(major, minor + 1, 0)
|
||
|
||
|
||
def _summary_markers_need_repair(text: str) -> bool:
|
||
"""判断顶部 SUMMARY 标记赋值是否损坏(空字符串、缺行或与 canonical 不一致)。"""
|
||
if re.search(r'^SUMMARY_BEGIN\s*=\s*""\s*(?:\r)?$', text, re.MULTILINE):
|
||
return True
|
||
if re.search(r"^SUMMARY_END\s*=", text, re.MULTILINE) is None:
|
||
return True
|
||
match = _SUMMARY_ASSIGNMENTS_PATTERN.search(text)
|
||
if not match:
|
||
return True
|
||
canonical = match.group(0).replace("\r\n", "\n").replace("\r", "\n")
|
||
return canonical != _SUMMARY_ASSIGNMENTS_BLOCK
|
||
|
||
|
||
def ensure_summary_marker_assignments(path: Path | None = None) -> bool:
|
||
"""修正 _version.py 顶部 SUMMARY_BEGIN/SUMMARY_END 赋值(防止空字符串或缺失)。"""
|
||
path = path or please_read_me_path()
|
||
text = path.read_text(encoding="utf-8")
|
||
if not _summary_markers_need_repair(text):
|
||
return False
|
||
match = _SUMMARY_ASSIGNMENTS_PATTERN.search(text)
|
||
if match:
|
||
updated = text[: match.start()] + _SUMMARY_ASSIGNMENTS_BLOCK + text[match.end() :]
|
||
else:
|
||
anchor = "# ==============================================================\n\n"
|
||
if anchor not in text:
|
||
return False
|
||
updated = text.replace(anchor, anchor + _SUMMARY_ASSIGNMENTS_BLOCK, 1)
|
||
if updated == text:
|
||
return False
|
||
path.write_text(updated, encoding="utf-8")
|
||
return True
|
||
|
||
|
||
def strip_summary_block(text: str) -> str:
|
||
"""从文本中移除 UPLOAD_SUMMARY 标记块。"""
|
||
begin = text.rfind(_UPLOAD_SUMMARY_BEGIN)
|
||
if begin == -1:
|
||
return text.rstrip() + "\n"
|
||
end = text.find(_UPLOAD_SUMMARY_END, begin)
|
||
if end == -1:
|
||
return text.rstrip() + "\n"
|
||
end += len(_UPLOAD_SUMMARY_END)
|
||
return (text[:begin] + text[end:]).rstrip() + "\n"
|
||
|
||
|
||
def write_summary_block(path: Path, title: str, bullets: list[str]) -> None:
|
||
"""将上传总结写入 _version.py 底部。"""
|
||
ensure_summary_marker_assignments(path)
|
||
text = strip_summary_block(path.read_text(encoding="utf-8"))
|
||
lines = [
|
||
"",
|
||
_UPLOAD_SUMMARY_BEGIN,
|
||
f"# TITLE: {title}",
|
||
"# BODY:",
|
||
]
|
||
for item in bullets:
|
||
lines.append(f"# - {item}")
|
||
lines.append(_UPLOAD_SUMMARY_END)
|
||
lines.append("")
|
||
path.write_text(text + "\n".join(lines), encoding="utf-8")
|
||
|
||
|
||
def remove_summary_block(path: Path) -> None:
|
||
"""从 _version.py 中移除 UPLOAD_SUMMARY 标记块。"""
|
||
text = path.read_text(encoding="utf-8")
|
||
path.write_text(strip_summary_block(text), encoding="utf-8")
|
||
|
||
|
||
def read_summary_for_commit(path: Path) -> tuple[str, list[str]]:
|
||
"""从 _version.py 解析 UPLOAD_SUMMARY 块的内容。"""
|
||
text = path.read_text(encoding="utf-8")
|
||
begin = text.rfind(_UPLOAD_SUMMARY_BEGIN)
|
||
end = text.find(_UPLOAD_SUMMARY_END, begin)
|
||
if begin == -1 or end == -1:
|
||
raise ValueError("_version.py 中缺少 UPLOAD_SUMMARY 标记块")
|
||
block = text[begin:end]
|
||
title = "Update"
|
||
bullets: list[str] = []
|
||
in_body = False
|
||
for raw in block.splitlines():
|
||
line = raw.strip()
|
||
if line.startswith("# TITLE:"):
|
||
title = line[len("# TITLE:") :].strip()
|
||
elif line.startswith("# BODY:"):
|
||
in_body = True
|
||
elif in_body and line.startswith("# -"):
|
||
bullets.append(line[3:].strip())
|
||
elif in_body and line.startswith("#") and not line.startswith("# ---"):
|
||
bullets.append(line.lstrip("# ").strip())
|
||
return title, bullets
|
||
|
||
|
||
def build_commit_message(version: str, title: str, bullets: list[str]) -> str:
|
||
"""构建 git commit 消息字符串。"""
|
||
first = f"v{version}: {title}"
|
||
if not bullets:
|
||
return first
|
||
return first + "\n\n" + "\n".join(f"- {b}" for b in bullets)
|
||
|
||
|
||
def classify_changed_paths(paths: list[str], package_dir_name: str = "games/endfield") -> bool:
|
||
"""判断变更路径列表中是否包含业务代码改动(非 _version.py 自身)。"""
|
||
prefix = f"{package_dir_name}/"
|
||
readme_names = {
|
||
f"{prefix}_version.py",
|
||
"_version.py",
|
||
"scripts/_version.py",
|
||
f"{prefix}please_read_me.py",
|
||
"please_read_me.py",
|
||
"scripts/please_read_me.py",
|
||
"scripts/upload_meta.py",
|
||
"scripts/version.py",
|
||
}
|
||
for raw in paths:
|
||
norm = raw.replace("\\", "/").strip()
|
||
if " -> " in norm:
|
||
norm = norm.split(" -> ")[-1].strip()
|
||
if norm in readme_names:
|
||
continue
|
||
if norm and norm != prefix.rstrip("/"):
|
||
return True
|
||
return False
|
||
|
||
|
||
def summarize_changes(paths: list[str]) -> tuple[str, list[str]]:
|
||
"""生成变更摘要信息(标题 + 更改列表)。"""
|
||
unique = sorted({p.replace("\\", "/") for p in paths if p.strip()})
|
||
if not unique:
|
||
return "维护性更新", ["无文件列表(请检查 git status)"]
|
||
|
||
bullets: list[str] = []
|
||
for p in unique:
|
||
if p.endswith("weapons.json"):
|
||
bullets.append("更新 weapons.json 武器数据")
|
||
elif p.endswith("characters.json"):
|
||
bullets.append("更新 characters.json 角色数据")
|
||
elif "multiplicative_zones" in p:
|
||
bullets.append(f"调整乘区逻辑 {p}")
|
||
elif p.endswith(".py"):
|
||
bullets.append(f"修改 {p}")
|
||
elif p.endswith(".md"):
|
||
bullets.append(f"更新文档 {p}")
|
||
else:
|
||
bullets.append(f"变更 {p}")
|
||
|
||
if len(bullets) == 1:
|
||
title = bullets[0]
|
||
else:
|
||
title = f"更新 {len(unique)} 处文件"
|
||
return title, bullets
|
||
|
||
|
||
def get_version() -> str:
|
||
"""获取项目版本号。"""
|
||
return _VERSION
|
||
|
||
|
||
def get_exe_version() -> str:
|
||
"""获取 EXE 版本号。"""
|
||
return _EXE_VERSION
|
||
|
||
|
||
# --- BEGIN UPLOAD_SUMMARY ---
|
||
# TITLE: 更新 7 处文件
|
||
# BODY:
|
||
# - 变更 .cursorrules
|
||
# - 更新文档 docs/上传脚本与-pre-commit.md
|
||
# - 更新文档 docs/会话接续手册.md
|
||
# - 更新文档 docs/操作指令集.md
|
||
# - 修改 games/endfield/tests/tools/test_upload_meta.py
|
||
# - 修改 scripts/_version.py
|
||
# - 修改 scripts/tools/github_upload_module.py
|
||
# --- END UPLOAD_SUMMARY ---
|