175 lines
5.8 KiB
Python
175 lines
5.8 KiB
Python
"""发布包清单与服务端发布元数据生成。"""
|
|
|
|
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())
|