version_log.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. r"""版本记录生成/校验 (2026-09-17 用户令: 源代码开发维护与打包版本管理)。
  4. ## 为什么不是"再手写一个 CHANGELOG.md"
  5. 手写版本记录的老毛病是**和代码里的版本号对不上**:改了 `src/version.py` 忘了改 md,或者反过来
  6. (本仓 0.4.0 期间就出现过三处版本号各说各话)。这里把事实只放在 `src/version.py` 的 `HISTORY` 里,
  7. `docs/版本记录.md` 由本器**生成**:
  8. python scripts/version_log.py # 打印(人看)
  9. python scripts/version_log.py --write # 写进 docs/版本记录.md(自动块)
  10. python scripts/version_log.py --check # 校验 md 与 src/version.py 是否一致(rc=6 表示不一致)
  11. `guanlan.py check` 与打包器都会调用 `--check` 这一条:**版本记录与版本号必须一致**,
  12. 否则交付包里会出现"包说 2.5.0、记录表最后一行是 2.4.0"这种事。
  13. 退出码: 0 一致/已写 · 6 不一致(需要跑 --write) · 2 文件缺失
  14. """
  15. from __future__ import annotations
  16. try:
  17. from app_common.app_common_guanlan.api import install_root as _install_root
  18. except ImportError: # 理论不可达;包结构异常时回退到按位置上跳
  19. from pathlib import Path as _P
  20. def _install_root(_f): return _P(_f).resolve().parents[4]
  21. import argparse
  22. import pathlib
  23. import sys
  24. ROOT = _install_root(__file__)
  25. sys.path.insert(0, str(ROOT))
  26. from src import version as V # noqa: E402
  27. DOC = ROOT / 'docs' / '版本记录.md'
  28. BEGIN, END = '<!-- VERSION-LOG:BEGIN -->', '<!-- VERSION-LOG:END -->'
  29. LEVEL_CN = {'major': '大', 'minor': '中', 'patch': '小', 'legacy': '旧编号', 'same': '无', '?': '?'}
  30. def body() -> str:
  31. """生成自动块正文(规则 + 当前版本 + 版本表 + 怎么改)。"""
  32. L = ['## 版本号规则(用户令 2026-09-17)', '',
  33. f'编号 **`{V.RULE}`**,例如 `v{V.VERSION}`。', '',
  34. '| 级别 | 含义 |', '|---|---|']
  35. for k in ('major', 'minor', 'patch'):
  36. # LEVEL_MEANING 里是"大版本号 —— 系统解决方案…"整句(与用户令措辞一致),
  37. # 表头已经有"级别"一列,这里只取破折号后的定义,免得读成"大版本号 | 大版本号 —— …"。
  38. L.append(f'| {LEVEL_CN[k]}版本号 | {V.LEVEL_MEANING[k].split(" —— ", 1)[-1]} |')
  39. L += ['', f'**怎么改**:{V.BUMP_RULE}', '',
  40. f'**打包文件名** = `{V.package_name()}`(规则:`{V.PACKAGE_STEM}_v<版本号>.zip`)。', '',
  41. f'版本号只写在 `src/version.py` 一行(`VERSION = \'{V.VERSION}\'`);'
  42. '打包器、安装脚本、安装记录 `install-info.json`、自检都读它,不另设真源。', '',
  43. '| 版本 | 日期 | 级别 | 主要变更 | 说明 |', '|---|---|---|---|---|']
  44. for h in V.HISTORY:
  45. L.append(f"| `v{h.get('version', '?')}` | {h.get('date', '?')} | {LEVEL_CN.get(h.get('level', '?'), '?')} | "
  46. f"{h.get('title', '')} | {h.get('note', '')} |")
  47. L += ['', '## 本次交付', '',
  48. f'* 版本:`v{V.VERSION}`({V.NAME})',
  49. f'* 交付包:`{V.package_name()}`',
  50. f'* 服务名:`{V.SERVICE_NAME}`(Windows 服务 / systemd 单元)',
  51. f'* 安装 / 卸载:`install.bat`、`install.sh` / `uninstall.bat`、`uninstall.sh`', '']
  52. return '\n'.join(L)
  53. def render() -> str:
  54. return (f'# 版本记录({V.NAME})\n\n'
  55. '> 本文件由 `scripts/version_log.py` 生成,**勿手改**:事实(版本号、级别、变更)写在\n'
  56. '> `src/version.py` 的 `HISTORY` 里,改完跑 `python scripts/version_log.py --write`。\n'
  57. '> `guanlan.py check` 与打包器都会校验本文件与代码里的版本号是否一致。\n\n'
  58. f'{BEGIN}\n{body()}{END}\n')
  59. def block_of(text: str) -> str:
  60. i, j = text.find(BEGIN), text.find(END)
  61. return text[i + len(BEGIN):j].strip() if i >= 0 and j > i else ''
  62. def main() -> int:
  63. ap = argparse.ArgumentParser(description='版本记录生成/校验')
  64. ap.add_argument('--write', action='store_true', help='写进 docs/版本记录.md')
  65. ap.add_argument('--check', action='store_true', help='只校验是否一致 (rc=6 不一致)')
  66. a = ap.parse_args()
  67. want = body().strip()
  68. if a.write:
  69. DOC.parent.mkdir(parents=True, exist_ok=True)
  70. DOC.write_text(render(), encoding='utf-8', newline='\n')
  71. print(f'已写入 {DOC.relative_to(ROOT).as_posix()}(版本 v{V.VERSION},{len(V.HISTORY)} 条历史)')
  72. return 0
  73. if a.check:
  74. if not DOC.is_file():
  75. print(f'[X] 缺 {DOC.relative_to(ROOT).as_posix()} —— 跑 python scripts/version_log.py --write')
  76. return 2
  77. got = block_of(DOC.read_text(encoding='utf-8'))
  78. if got != want:
  79. print(f'[X] 版本记录与 src/version.py 不一致(版本 v{V.VERSION})'
  80. f' —— 跑 python scripts/version_log.py --write')
  81. return 6
  82. # ★2026-09-29 加:根 `VERSION` 文件曾被漏在 0.2.0(历史遗留),与真源脱节 →
  83. # 它是给外部工具看的**镜像标记**,必须与 version.py 同号(真源永远只有 version.py 一处)。
  84. vf = ROOT / 'VERSION'
  85. if vf.is_file():
  86. import re as _re
  87. m = _re.search(r'([0-9]+\.[0-9]+\.[0-9]+)', vf.read_text(encoding='utf-8'))
  88. if m and m.group(1) != V.VERSION:
  89. print(f'[X] 根 VERSION 文件是 {m.group(1)},与版本真源 {V.VERSION} 不一致'
  90. f' —— 真源只有 src/version.py 一处,VERSION 是它的镜像')
  91. return 6
  92. print(f'[OK] 版本记录一致(v{V.VERSION},{len(V.HISTORY)} 条)')
  93. return 0
  94. print(render())
  95. return 0
  96. if __name__ == '__main__':
  97. sys.exit(main())