"""发布包清单与服务端发布元数据生成。""" import argparse import hashlib import json from pathlib import Path PACKAGE_FORMAT = "cmshopee-portable-v1" UPDATER_PROTOCOL = 1 ENTRY_POINT = "cmshopee.exe" MANIFEST_FILENAME = "package-manifest.json" ALLOWED_ROOTS = { ENTRY_POINT, "_internal", "version.txt", "README.txt", MANIFEST_FILENAME, "cmshopee-updater.exe", } FORBIDDEN_ROOTS = {"data", ".cmshopee-update"} class ReleaseManifestError(ValueError): """发布目录不符合自动升级包契约。""" def sha256_file(path): digest = hashlib.sha256() with Path(path).open("rb") as stream: for chunk in iter(lambda: stream.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest() def _normalized_relative(path, root): relative = path.relative_to(root).as_posix() if relative.startswith("/") or ".." in Path(relative).parts: raise ReleaseManifestError("发布包包含不安全的文件路径:%s" % relative) return relative def build_package_manifest(release_dir, app_version): release_dir = Path(release_dir).resolve() if not release_dir.is_dir(): raise ReleaseManifestError("发布目录不存在:%s" % release_dir) required = [ENTRY_POINT, "_internal", "version.txt", "README.txt"] missing = [name for name in required if not (release_dir / name).exists()] if missing: raise ReleaseManifestError("发布目录缺少必要项目:%s" % "、".join(missing)) if not (release_dir / "_internal").is_dir(): raise ReleaseManifestError("_internal 必须是目录") version_text = (release_dir / "version.txt").read_text( encoding="utf-8-sig" ).strip() if version_text != app_version: raise ReleaseManifestError( "version.txt 与应用版本不一致:%s != %s" % (version_text, app_version) ) root_names = {entry.name for entry in release_dir.iterdir()} forbidden = sorted(root_names & FORBIDDEN_ROOTS) if forbidden: raise ReleaseManifestError("发布目录包含用户数据:%s" % "、".join(forbidden)) unknown = sorted(root_names - ALLOWED_ROOTS) if unknown: raise ReleaseManifestError("发布目录包含未授权根项目:%s" % "、".join(unknown)) files = [] seen = set() for path in sorted(release_dir.rglob("*"), key=lambda value: value.as_posix().lower()): if path.is_symlink(): raise ReleaseManifestError("发布包不允许符号链接:%s" % path) if not path.is_file() or path.name == MANIFEST_FILENAME: continue relative = _normalized_relative(path, release_dir) key = relative.casefold() if key in seen: raise ReleaseManifestError("发布包包含重复路径:%s" % relative) seen.add(key) files.append( { "path": relative, "size_bytes": path.stat().st_size, "sha256": sha256_file(path), } ) replace_roots = sorted(root_names | {MANIFEST_FILENAME}, key=str.casefold) return { "schema_version": 1, "package_format": PACKAGE_FORMAT, "app_version": app_version, "entry_point": ENTRY_POINT, "updater_protocol": UPDATER_PROTOCOL, "min_updater_protocol": UPDATER_PROTOCOL, "replace_roots": replace_roots, "files": files, "signature_algorithm": "", "manifest_signature": "", } def write_package_manifest(release_dir, app_version): release_dir = Path(release_dir) manifest = build_package_manifest(release_dir, app_version) output = release_dir / MANIFEST_FILENAME output.write_text( json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", ) return output def build_release_metadata(zip_path, app_version): zip_path = Path(zip_path) if not zip_path.is_file(): raise ReleaseManifestError("发布压缩包不存在:%s" % zip_path) return { "platform": "windows", "release": { "version": app_version, "force_update": False, "min_supported_version": "", "download_url": "", "download_filename": zip_path.name, "sha256": sha256_file(zip_path), "size_bytes": zip_path.stat().st_size, "package_format": PACKAGE_FORMAT, "updater_protocol": UPDATER_PROTOCOL, "min_updater_protocol": UPDATER_PROTOCOL, "release_notes": "", "signature_algorithm": "", "manifest_signature": "", }, } def write_release_metadata(zip_path, app_version, output_path): metadata = build_release_metadata(zip_path, app_version) output_path = Path(output_path) output_path.write_text( json.dumps(metadata, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", ) return output_path def main(argv=None): parser = argparse.ArgumentParser(description="生成蝦皮圈优化助手发布校验文件") subparsers = parser.add_subparsers(dest="command", required=True) manifest_parser = subparsers.add_parser("manifest") manifest_parser.add_argument("--release-dir", required=True) manifest_parser.add_argument("--version", required=True) metadata_parser = subparsers.add_parser("metadata") metadata_parser.add_argument("--zip", required=True) metadata_parser.add_argument("--version", required=True) metadata_parser.add_argument("--output", required=True) args = parser.parse_args(argv) if args.command == "manifest": write_package_manifest(args.release_dir, args.version) else: write_release_metadata(args.zip, args.version, args.output) return 0 if __name__ == "__main__": raise SystemExit(main())