pack_module.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. r"""P13-2:模块级打包器 `scripts/pack_module.py` + 四模块 `pack.bat`/`pack.sh` 包装。
  4. 设计(与既有整体打包 `scripts/pack_dist.py` 的排除规则一致):
  5. · 每个系统模块(app_frontEnd / app_backEnd / app_algorithmModel / app_ETL)可**单独打包**为
  6. `dist/app_<模块>_v<版本>.zip`,内含:该模块目录(排除缓存/数据/产物/日志/临时件)+ `module_manifest.json`
  7. (名称、版本、生成时间、文件数、逐文件 sha256、字节数、依赖信息);
  8. · `--all` 一次打四模块;`--verify <zip>` 开箱核验(逐文件比对 sha256);
  9. · 版本单一真源:`app_common/app_common_guanlan/version.py` 的 VERSION(与根 VERSION 一致)。
  10. 用法:
  11. python scripts/pack_module.py --module app_ETL
  12. python scripts/pack_module.py --all
  13. python scripts/pack_module.py --verify dist/app_ETL_v2.50.0.zip
  14. """
  15. from __future__ import annotations
  16. import argparse
  17. import hashlib
  18. import json
  19. import pathlib
  20. import re
  21. import sys
  22. import time
  23. import zipfile
  24. sys.stdout.reconfigure(encoding='utf-8', errors='replace')
  25. ROOT = pathlib.Path(__file__).resolve().parents[1]
  26. MODULES = ('app_frontEnd', 'app_backEnd', 'app_algorithmModel', 'app_ETL')
  27. # 排除规则(与 pack_dist.py 同口径:不入包的东西)
  28. EXCLUDE_DIRS = {'__pycache__', '.git', '.venv', 'node_modules', 'logs', 'run', 'outputs'}
  29. # ★P13:不排除 target —— Java 后端/网关的编译产物与依赖 jar 就在 target/{classes,lib},
  30. # 排掉会让 app_backEnd 的包跑不起来(实逮)。
  31. EXCLUDE_RE = re.compile(r'(\*\.tmp|\.bak.*|\.old$|\.orig$|\.rej$|\.swp$|~$|^~\$|\.DS_Store$|\.log$|\.jsonl$|_cache$)')
  32. def pkg_version() -> str:
  33. vf = ROOT / 'app_common/app_common_guanlan/version.py'
  34. m = re.search(r"^VERSION\s*=\s*'([^']+)'", vf.read_text(encoding='utf-8'), re.M)
  35. if m:
  36. return m.group(1)
  37. rv = (ROOT / 'VERSION').read_text(encoding='utf-8').strip()
  38. return rv or '0.0.0'
  39. def ext_includes(mod: str) -> list[pathlib.Path]:
  40. """模块外、但属于该模块运行必需的产物(如前端 Vue 产物 release/web)。由 <module>/pack_include.json 声明。"""
  41. j = ROOT / mod / 'pack_include.json'
  42. if not j.exists():
  43. return []
  44. try:
  45. spec = json.loads(j.read_text(encoding='utf-8'))
  46. except Exception:
  47. return []
  48. out = []
  49. for rel in spec.get('include', []):
  50. q = ROOT / rel
  51. if q.is_dir():
  52. out += [x for x in sorted(q.rglob('*')) if x.is_file()]
  53. elif q.is_file():
  54. out.append(q)
  55. return out
  56. def included_files(mod: str, with_data: bool) -> list[pathlib.Path]:
  57. base = ROOT / mod
  58. out = []
  59. for p in sorted(base.rglob('*')):
  60. if not p.is_file():
  61. continue
  62. rel = p.relative_to(base)
  63. if any(part in EXCLUDE_DIRS for part in rel.parts):
  64. continue
  65. if not with_data and rel.parts and rel.parts[0] in ('data',):
  66. continue
  67. if EXCLUDE_RE.search(p.name):
  68. continue
  69. out.append(p)
  70. return out
  71. def sha256(p: pathlib.Path) -> str:
  72. h = hashlib.sha256()
  73. with p.open('rb') as f:
  74. for chunk in iter(lambda: f.read(1 << 20), b''):
  75. h.update(chunk)
  76. return h.hexdigest()
  77. def pack(mod: str, outdir: pathlib.Path, with_data: bool = False) -> pathlib.Path:
  78. assert mod in MODULES, '模块名必须是 %s 之一' % (MODULES,)
  79. ver = pkg_version()
  80. outdir.mkdir(parents=True, exist_ok=True)
  81. zp = outdir / ('app_%s_v%s.zip' % (mod, ver))
  82. files = included_files(mod, with_data)
  83. exts = ext_includes(mod)
  84. files = files + [q for q in exts if q not in files]
  85. man = {'module': mod, 'version': ver, 'built_at': time.strftime('%Y-%m-%d %H:%M:%S'),
  86. 'with_data': with_data, 'file_count': len(files), 'files': {}, 'root': mod}
  87. for p in files:
  88. rel = str(p.relative_to(ROOT)).replace('\\', '/')
  89. man['files'][rel] = {'sha256': sha256(p), 'size': p.stat().st_size}
  90. with zipfile.ZipFile(zp, 'w', zipfile.ZIP_DEFLATED) as z:
  91. for p in files:
  92. z.write(p, str(p.relative_to(ROOT)).replace('\\', '/'))
  93. z.writestr('%s/module_manifest.json' % mod, json.dumps(man, ensure_ascii=False, indent=2))
  94. print(' [OK] %-22s %8d 文件 · %9d B → %s' % (mod, len(files), zp.stat().st_size, zp.name))
  95. return zp
  96. def verify(zp: pathlib.Path) -> int:
  97. print('核验 %s' % zp)
  98. bad = 0
  99. with zipfile.ZipFile(zp) as z:
  100. man_name = next((n for n in z.namelist() if n.endswith('module_manifest.json')), None)
  101. if not man_name:
  102. print(' [X] 包内没有 module_manifest.json')
  103. return 1
  104. man = json.loads(z.read(man_name).decode('utf-8'))
  105. print(' 模块=%s 版本=%s 文件数(声明)=%d' % (man['module'], man['version'], man['file_count']))
  106. for rel, meta in man['files'].items():
  107. try:
  108. data = z.read(rel)
  109. except KeyError:
  110. print(' [X] 缺文件 %s' % rel)
  111. bad += 1
  112. continue
  113. h = hashlib.sha256(data).hexdigest()
  114. if h != meta['sha256']:
  115. print(' [X] 指纹不符 %s' % rel)
  116. bad += 1
  117. print(' %s' % ('全部一致' if not bad else '有 %d 处不一致' % bad))
  118. return 0 if not bad else 1
  119. def main() -> int:
  120. ap = argparse.ArgumentParser(description='观澜 · 模块级打包')
  121. ap.add_argument('--module', choices=MODULES)
  122. ap.add_argument('--all', action='store_true')
  123. ap.add_argument('--out', default=str(ROOT / 'dist'))
  124. ap.add_argument('--with-data', action='store_true', help='连模块 data/ 一起打(默认不含)')
  125. ap.add_argument('--verify')
  126. a = ap.parse_args()
  127. if a.verify:
  128. return verify(pathlib.Path(a.verify))
  129. outdir = pathlib.Path(a.out)
  130. print('观澜 · 模块打包 · 版本 %s · 输出 %s' % (pkg_version(), outdir))
  131. targets = list(MODULES) if a.all else ([a.module] if a.module else [])
  132. if not targets:
  133. ap.print_help()
  134. return 2
  135. for m in targets:
  136. pack(m, outdir, a.with_data)
  137. return 0
  138. if __name__ == '__main__':
  139. raise SystemExit(main())