| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- r"""P13-2:模块级打包器 `scripts/pack_module.py` + 四模块 `pack.bat`/`pack.sh` 包装。
- 设计(与既有整体打包 `scripts/pack_dist.py` 的排除规则一致):
- · 每个系统模块(app_frontEnd / app_backEnd / app_algorithmModel / app_ETL)可**单独打包**为
- `dist/app_<模块>_v<版本>.zip`,内含:该模块目录(排除缓存/数据/产物/日志/临时件)+ `module_manifest.json`
- (名称、版本、生成时间、文件数、逐文件 sha256、字节数、依赖信息);
- · `--all` 一次打四模块;`--verify <zip>` 开箱核验(逐文件比对 sha256);
- · 版本单一真源:`app_common/app_common_guanlan/version.py` 的 VERSION(与根 VERSION 一致)。
- 用法:
- python scripts/pack_module.py --module app_ETL
- python scripts/pack_module.py --all
- python scripts/pack_module.py --verify dist/app_ETL_v2.50.0.zip
- """
- from __future__ import annotations
- import argparse
- import hashlib
- import json
- import pathlib
- import re
- import sys
- import time
- import zipfile
- sys.stdout.reconfigure(encoding='utf-8', errors='replace')
- ROOT = pathlib.Path(__file__).resolve().parents[1]
- MODULES = ('app_frontEnd', 'app_backEnd', 'app_algorithmModel', 'app_ETL')
- # 排除规则(与 pack_dist.py 同口径:不入包的东西)
- EXCLUDE_DIRS = {'__pycache__', '.git', '.venv', 'node_modules', 'logs', 'run', 'outputs'}
- # ★P13:不排除 target —— Java 后端/网关的编译产物与依赖 jar 就在 target/{classes,lib},
- # 排掉会让 app_backEnd 的包跑不起来(实逮)。
- EXCLUDE_RE = re.compile(r'(\*\.tmp|\.bak.*|\.old$|\.orig$|\.rej$|\.swp$|~$|^~\$|\.DS_Store$|\.log$|\.jsonl$|_cache$)')
- def pkg_version() -> str:
- vf = ROOT / 'app_common/app_common_guanlan/version.py'
- m = re.search(r"^VERSION\s*=\s*'([^']+)'", vf.read_text(encoding='utf-8'), re.M)
- if m:
- return m.group(1)
- rv = (ROOT / 'VERSION').read_text(encoding='utf-8').strip()
- return rv or '0.0.0'
- def ext_includes(mod: str) -> list[pathlib.Path]:
- """模块外、但属于该模块运行必需的产物(如前端 Vue 产物 release/web)。由 <module>/pack_include.json 声明。"""
- j = ROOT / mod / 'pack_include.json'
- if not j.exists():
- return []
- try:
- spec = json.loads(j.read_text(encoding='utf-8'))
- except Exception:
- return []
- out = []
- for rel in spec.get('include', []):
- q = ROOT / rel
- if q.is_dir():
- out += [x for x in sorted(q.rglob('*')) if x.is_file()]
- elif q.is_file():
- out.append(q)
- return out
- def included_files(mod: str, with_data: bool) -> list[pathlib.Path]:
- base = ROOT / mod
- out = []
- for p in sorted(base.rglob('*')):
- if not p.is_file():
- continue
- rel = p.relative_to(base)
- if any(part in EXCLUDE_DIRS for part in rel.parts):
- continue
- if not with_data and rel.parts and rel.parts[0] in ('data',):
- continue
- if EXCLUDE_RE.search(p.name):
- continue
- out.append(p)
- return out
- def sha256(p: pathlib.Path) -> str:
- h = hashlib.sha256()
- with p.open('rb') as f:
- for chunk in iter(lambda: f.read(1 << 20), b''):
- h.update(chunk)
- return h.hexdigest()
- def pack(mod: str, outdir: pathlib.Path, with_data: bool = False) -> pathlib.Path:
- assert mod in MODULES, '模块名必须是 %s 之一' % (MODULES,)
- ver = pkg_version()
- outdir.mkdir(parents=True, exist_ok=True)
- zp = outdir / ('app_%s_v%s.zip' % (mod, ver))
- files = included_files(mod, with_data)
- exts = ext_includes(mod)
- files = files + [q for q in exts if q not in files]
- man = {'module': mod, 'version': ver, 'built_at': time.strftime('%Y-%m-%d %H:%M:%S'),
- 'with_data': with_data, 'file_count': len(files), 'files': {}, 'root': mod}
- for p in files:
- rel = str(p.relative_to(ROOT)).replace('\\', '/')
- man['files'][rel] = {'sha256': sha256(p), 'size': p.stat().st_size}
- with zipfile.ZipFile(zp, 'w', zipfile.ZIP_DEFLATED) as z:
- for p in files:
- z.write(p, str(p.relative_to(ROOT)).replace('\\', '/'))
- z.writestr('%s/module_manifest.json' % mod, json.dumps(man, ensure_ascii=False, indent=2))
- print(' [OK] %-22s %8d 文件 · %9d B → %s' % (mod, len(files), zp.stat().st_size, zp.name))
- return zp
- def verify(zp: pathlib.Path) -> int:
- print('核验 %s' % zp)
- bad = 0
- with zipfile.ZipFile(zp) as z:
- man_name = next((n for n in z.namelist() if n.endswith('module_manifest.json')), None)
- if not man_name:
- print(' [X] 包内没有 module_manifest.json')
- return 1
- man = json.loads(z.read(man_name).decode('utf-8'))
- print(' 模块=%s 版本=%s 文件数(声明)=%d' % (man['module'], man['version'], man['file_count']))
- for rel, meta in man['files'].items():
- try:
- data = z.read(rel)
- except KeyError:
- print(' [X] 缺文件 %s' % rel)
- bad += 1
- continue
- h = hashlib.sha256(data).hexdigest()
- if h != meta['sha256']:
- print(' [X] 指纹不符 %s' % rel)
- bad += 1
- print(' %s' % ('全部一致' if not bad else '有 %d 处不一致' % bad))
- return 0 if not bad else 1
- def main() -> int:
- ap = argparse.ArgumentParser(description='观澜 · 模块级打包')
- ap.add_argument('--module', choices=MODULES)
- ap.add_argument('--all', action='store_true')
- ap.add_argument('--out', default=str(ROOT / 'dist'))
- ap.add_argument('--with-data', action='store_true', help='连模块 data/ 一起打(默认不含)')
- ap.add_argument('--verify')
- a = ap.parse_args()
- if a.verify:
- return verify(pathlib.Path(a.verify))
- outdir = pathlib.Path(a.out)
- print('观澜 · 模块打包 · 版本 %s · 输出 %s' % (pkg_version(), outdir))
- targets = list(MODULES) if a.all else ([a.module] if a.module else [])
- if not targets:
- ap.print_help()
- return 2
- for m in targets:
- pack(m, outdir, a.with_data)
- return 0
- if __name__ == '__main__':
- raise SystemExit(main())
|