mirror of
https://github.com/EstrellaXD/Auto_Bangumi.git
synced 2026-10-29 03:36:15 +08:00
- 拒绝符号链接形式的条目;PR 检查纳入类型变更(T) - PR 头部先解析为固定 SHA,作者代码不能借 FETCH_HEAD 换掉被检查的 commit - 使用已发布的 catalog 前先验签,篡改的 release 资源不会被重新签名 - 全部下架时签发空目录
97 lines
3.7 KiB
Python
97 lines
3.7 KiB
Python
"""生成并签名插件目录(``plugins`` tag 的发布产物)。
|
||
|
||
用法(需要更新签名私钥,见 CLAUDE.md 的发布说明)::
|
||
|
||
uv run --no-project --with cryptography python scripts/build_plugin_catalog.py \
|
||
--key ~/.autobangumi/update-signing-key.pem \
|
||
--out release-assets dist/*.zip
|
||
|
||
输入是 ``ab-plugin pack`` 打出的 zip;``--sources`` 是 ``plugin_registry.py build``
|
||
写出的 ``sources.json``(每个插件的 repo、commit 与 README)。输出目录里有 ``catalog.json``、每个 zip 以及
|
||
它们的 ``.sig``(对文件全部字节做 ed25519 签名,base64 文本),一并上传到 GitHub
|
||
release ``plugins`` 即可。安装端的校验见 ``backend/src/module/plugin/installer.py``。
|
||
"""
|
||
|
||
import argparse
|
||
import base64
|
||
import hashlib
|
||
import json
|
||
import shutil
|
||
import tomllib
|
||
import zipfile
|
||
from pathlib import Path
|
||
|
||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
||
|
||
CATALOG_SCHEMA = 2
|
||
# semver 中 4.0.0-beta.N < 4.0.0,写 4.0.0 会把全部 4.0 beta 宿主挡在外面
|
||
DEFAULT_MIN_AB = "4.0.0-beta.1"
|
||
|
||
|
||
def _entry(archive: Path, min_ab: str, sources: dict[str, dict[str, str]]) -> dict:
|
||
with zipfile.ZipFile(archive) as zf:
|
||
plugin = tomllib.loads(zf.read("plugin.toml").decode("utf-8"))["plugin"]
|
||
source = sources.get(plugin["id"], {})
|
||
return {
|
||
"id": plugin["id"],
|
||
"name": plugin["name"],
|
||
"version": plugin["version"],
|
||
"kind": "plugin",
|
||
"extension_points": plugin.get("extension_points", []),
|
||
"sdk": plugin["sdk"],
|
||
"min_ab_version": min_ab,
|
||
"description": plugin.get("description", ""),
|
||
"authors": plugin.get("authors", []),
|
||
"permissions": plugin.get("permissions", []),
|
||
"has_web": bool(plugin.get("ui")),
|
||
"repo": source.get("repo", ""),
|
||
"commit": source.get("commit", ""),
|
||
"path": source.get("path", "."),
|
||
"readme": source.get("readme", ""),
|
||
"asset": archive.name,
|
||
"sha256": hashlib.sha256(archive.read_bytes()).hexdigest(),
|
||
}
|
||
|
||
|
||
def _sign(key: Ed25519PrivateKey, path: Path) -> None:
|
||
signature = key.sign(path.read_bytes())
|
||
Path(str(path) + ".sig").write_text(base64.b64encode(signature).decode() + "\n")
|
||
|
||
|
||
def build(
|
||
archives: list[Path],
|
||
out: Path,
|
||
key_path: Path,
|
||
min_ab: str = DEFAULT_MIN_AB,
|
||
sources: dict[str, dict[str, str]] | None = None,
|
||
) -> Path:
|
||
key = load_pem_private_key(key_path.read_bytes(), password=None)
|
||
if not isinstance(key, Ed25519PrivateKey):
|
||
raise SystemExit("签名密钥必须是 ed25519")
|
||
out.mkdir(parents=True, exist_ok=True)
|
||
entries = []
|
||
for archive in sorted(archives):
|
||
entries.append(_entry(archive, min_ab, sources or {}))
|
||
shutil.copy2(archive, out / archive.name)
|
||
_sign(key, out / archive.name)
|
||
catalog = out / "catalog.json"
|
||
catalog.write_text(
|
||
json.dumps({"schema": CATALOG_SCHEMA, "plugins": entries}, indent=2) + "\n",
|
||
encoding="utf-8",
|
||
)
|
||
_sign(key, catalog)
|
||
return catalog
|
||
|
||
|
||
if __name__ == "__main__":
|
||
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||
parser.add_argument("archives", nargs="*", type=Path) # 全部下架时为空
|
||
parser.add_argument("--key", required=True, type=Path)
|
||
parser.add_argument("--out", required=True, type=Path)
|
||
parser.add_argument("--min-ab", default=DEFAULT_MIN_AB)
|
||
parser.add_argument("--sources", type=Path)
|
||
args = parser.parse_args()
|
||
sources = json.loads(args.sources.read_text("utf-8")) if args.sources else None
|
||
print(build(args.archives, args.out, args.key, args.min_ab, sources))
|