Files
panel-project-deploy/scripts/build_delivery_package.py

170 lines
6.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
交付压缩包构建脚本(合同对齐)。
产物(位于 --out-dir,默认 .Codex/delivery/):
1) <zip-name> 给甲方/终端使用的交付压缩包
2) 交付清单.md 与 zip 并列的单独文件(不打入包内)
zip 内部固定结构:
README.md 包根目录唯一一份 README
docs/screenshots/ README 引用的全部配图(解压可显示)
截图/界面截图/ 当前产品界面截图(中性文件名)
录屏/
文档/环境依赖清单.md
文档/部署注意事项.md
用法见 --help。
"""
from __future__ import annotations
import argparse
import os
import re
import shutil
import sys
import tempfile
import zipfile
from pathlib import Path
def copy_file(src: Path, dst: Path):
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, dst)
def main() -> int:
ap = argparse.ArgumentParser(description="构建交付压缩包与并列交付清单")
ap.add_argument("--project-dir", required=True)
ap.add_argument("--readme", default="README.md")
ap.add_argument("--out-dir", default=".Codex/delivery")
ap.add_argument("--assets-product", default=".Codex/delivery/assets/product")
ap.add_argument("--recordings", default=".Codex/delivery/recordings")
ap.add_argument("--dep-doc", default=".Codex/delivery/环境依赖清单.md")
ap.add_argument("--notes-doc", default=".Codex/delivery/部署注意事项.md")
ap.add_argument("--manifest-src", default=".Codex/delivery/manifest.md")
ap.add_argument("--zip-name", default=None, help="默认 smart-parking-delivery-<YYYYMMDD>.zip")
ap.add_argument("--video-name", default="full-feature-demo.mp4")
args = ap.parse_args()
root = Path(args.project_dir).resolve()
out_dir = root / args.out_dir
out_dir.mkdir(parents=True, exist_ok=True)
readme_path = root / args.readme
if not readme_path.exists():
print("[FAIL] README 不存在:%s" % readme_path)
return 1
zip_name = args.zip_name or ("smart-parking-delivery-%s.zip" % _today())
zip_path = out_dir / zip_name
with tempfile.TemporaryDirectory() as td:
stage = Path(td)
# 1) README(包根唯一一份)
copy_file(readme_path, stage / "README.md")
# 2) README 引用图片 -> docs/...(保持相对路径)
readme_text = readme_path.read_text(encoding="utf-8", errors="ignore")
img_refs = re.findall(r"!\[[^\]]*\]\(([^)]+)\)", readme_text)
copied_img = set()
for ref in img_refs:
ref = ref.strip()
if "://" in ref or ref.startswith("#"):
continue
src_img = (readme_path.parent / ref).resolve()
if not src_img.exists():
print("[FAIL] README 引用图片缺失:%s" % ref)
return 1
rel = src_img.relative_to(root)
if str(rel) not in copied_img:
copy_file(src_img, stage / rel)
copied_img.add(str(rel))
# 3) 产品界面截图(中性命名,不体现版本对比或来源宣传)
product_assets = root / args.assets_product
if product_assets.is_dir():
shutil.copytree(product_assets, stage / "截图" / "界面截图", dirs_exist_ok=True)
# 4) 录屏(只取指定 mp4)
rec_dir = root / args.recordings
video = rec_dir / args.video_name
if video.exists():
copy_file(video, stage / "录屏" / args.video_name)
# 5) 文档目录:仅依赖清单 + 注意事项(无 README、无交付清单)
for d in (args.dep_doc, args.notes_doc):
p = root / d
if p.exists():
copy_file(p, stage / "文档" / p.name)
# 打包
if zip_path.exists():
zip_path.unlink()
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as z:
for p in sorted(stage.rglob("*")):
if p.is_file():
z.write(p, p.relative_to(stage).as_posix())
# 交付清单:复制为与 zip 并列的单独文件
manifest_src = root / args.manifest_src
checklist_path = out_dir / "交付清单.md"
if manifest_src.exists():
shutil.copy2(manifest_src, checklist_path)
# 校验:解压核对 README 图片
bad = _verify_zip(zip_path)
if bad:
print("[FAIL] 交付包校验未通过:%s" % bad)
return 1
print("交付包:%s (%.2f MB)" % (zip_path, zip_path.stat().st_size/1024/1024))
if checklist_path.exists():
print("并列交付清单:%s" % checklist_path)
return 0
def _today() -> str:
from datetime import datetime
return datetime.now().strftime("%Y%m%d")
def _verify_zip(zip_path: Path) -> list[str]:
problems: list[str] = []
with tempfile.TemporaryDirectory() as td:
with zipfile.ZipFile(zip_path) as z:
z.extractall(td)
v = Path(td)
names = [p.relative_to(v).as_posix() for p in v.rglob("*") if p.is_file()]
readmes = [n for n in names if n.lower().endswith("readme.md")]
if readmes != ["README.md"]:
problems.append("README 必须且只能在包根一份,实际:%s" % readmes)
if any("交付清单" in n for n in names):
problems.append("交付清单不得打入包内")
if any(re.search(r"(before|after|改造前|原版)", n, re.I) for n in names):
problems.append("包内不得出现对比期/before/after 文件命名")
docs = [n for n in names if n.startswith("文档/")]
if any(n.lower().endswith("readme.md") for n in docs):
problems.append("文档目录不得放 README")
if (v / "README.md").exists():
txt = (v / "README.md").read_text(encoding="utf-8", errors="ignore")
for ref in re.findall(r"!\[[^\]]*\]\(([^)]+)\)", txt):
if "://" in ref or ref.startswith("#"):
continue
if not (v / ref).exists():
problems.append("README 图片解压后缺失:%s" % ref)
forbidden_in_readme = [
"致谢", "原始开源项目", "原项目链接", "原作者", "基于开源",
"He Rui", "CrazyHer", "campus-surveillance",
]
for term in forbidden_in_readme:
if term.lower() in txt.lower():
problems.append("README 出现不应主动展示的来源表述:%s" % term)
return problems
if __name__ == "__main__":
sys.exit(main())