mirror of
https://github.com/shy3130/tick-stock-panel.git
synced 2026-11-02 02:16:13 +00:00
86 lines
3.1 KiB
Python
86 lines
3.1 KiB
Python
#!/usr/bin/env python3
|
|
"""生成升级保护清单 — 全量程序文件 sha256 基线 (app/upgrade_manifest.json)。
|
|
|
|
供发布流水线调用 (release.yml 桌面打包 / Dockerfile 镜像构建), 运行时由
|
|
app/services/upgrade_guard.py 读取:
|
|
scripts/generate_manifest.py --root /app --output /app/app/upgrade_manifest.json \
|
|
--version-file VERSION
|
|
|
|
排除规则必须与 upgrade_guard.EXCLUDED_* 保持同集, 否则会把运行期产物
|
|
当成用户改动。清单自身也不入清单。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
EXCLUDED_DIRS = {"data", ".venv", "venv", "node_modules", "__pycache__", ".git", "logs"}
|
|
EXCLUDED_NAMES = {".env", ".DS_Store", "upgrade_manifest.json"}
|
|
EXCLUDED_SUFFIXES = {".pyc", ".log", ".tmp"}
|
|
|
|
|
|
def _is_excluded(rel: str, name: str) -> bool:
|
|
if rel in EXCLUDED_DIRS or rel.split("/", 1)[0] in EXCLUDED_DIRS:
|
|
return True
|
|
if name in EXCLUDED_NAMES:
|
|
return True
|
|
return Path(name).suffix in EXCLUDED_SUFFIXES
|
|
|
|
|
|
def _sha256(path: Path) -> str:
|
|
h = hashlib.sha256()
|
|
with path.open("rb") as f:
|
|
for chunk in iter(lambda: f.read(1 << 20), b""):
|
|
h.update(chunk)
|
|
return h.hexdigest()
|
|
|
|
|
|
def generate(root: Path, *, version: str) -> dict[str, object]:
|
|
files: dict[str, str] = {}
|
|
for path in sorted(root.rglob("*")):
|
|
if not path.is_file():
|
|
continue
|
|
rel = path.relative_to(root).as_posix()
|
|
if _is_excluded(rel, path.name):
|
|
continue
|
|
files[rel] = _sha256(path)
|
|
return {
|
|
"version": version,
|
|
"generated_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
"file_count": len(files),
|
|
"files": files,
|
|
}
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--root", required=True, type=Path, help="程序文件根目录 (清单的对比基准)")
|
|
parser.add_argument("--output", required=True, type=Path, help="清单输出路径 (应位于 <root>/app/ 下)")
|
|
parser.add_argument("--version", default="", help="版本号 (与 --version-file 二选一)")
|
|
parser.add_argument("--version-file", type=Path, help="从文件读版本 (如仓库根 VERSION)")
|
|
args = parser.parse_args(argv)
|
|
|
|
version = args.version.strip()
|
|
if not version and args.version_file and args.version_file.is_file():
|
|
version = args.version_file.read_text(encoding="utf-8").strip()
|
|
if not version:
|
|
parser.error("需要 --version 或可读的 --version-file")
|
|
|
|
root = args.root.resolve()
|
|
if not root.is_dir():
|
|
parser.error(f"--root 不是目录: {root}")
|
|
|
|
manifest = generate(root, version=version)
|
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
args.output.write_text(json.dumps(manifest, ensure_ascii=False, indent=1), encoding="utf-8")
|
|
print(f"manifest written: {args.output} ({manifest['file_count']} files, version {version})")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|