Files
MyGoNavi/tools/generate-winget-cli-manifest.py
Syngnat 4d5c0e6bb9 feat(cli): 新增独立命令行与发布链
- 新增无头运行时及连接、查询、导出、批处理、审计和 MCP 命令
- 复用活动数据根、密文存储与跨进程锁,落实写入安全和取消语义
- 增加六平台 CLI 归档、独立校验和、Docker、npm 与 WinGet 分发
- 隔离 GUI/CLI 更新资产并强化 macOS 签名与公证门禁
- 补充并发、审计、事务及发布契约回归测试

Refs #902
2026-08-11 10:34:58 +08:00

121 lines
4.7 KiB
Python

#!/usr/bin/env python3
"""Generate a WinGet manifest for the standalone GoNavi CLI.
The input is the independent CLI checksum file published with a stable
release. No platform hash is accepted from command-line text, which keeps
the generated manifest tied to the release's signed asset contract.
"""
from __future__ import annotations
import argparse
import re
from pathlib import Path
CLI_ASSETS = {
"x64": "gonavi-cli_{version}_windows_amd64.zip",
"arm64": "gonavi-cli_{version}_windows_arm64.zip",
}
CHECKSUM_NAME = "gonavi-cli_{version}_checksums.txt"
BINARY_NAME = "gonavi.exe"
VERSION_RE = re.compile(r"^\d+\.\d+\.\d+$")
CHECKSUM_RE = re.compile(r"^([0-9a-fA-F]{64})\s+\*?(.+)$")
def load_checksums(path: Path, version: str) -> dict[str, str]:
expected_checksum_name = CHECKSUM_NAME.format(version=version)
if path.name != expected_checksum_name:
raise ValueError(f"checksum file must be named {expected_checksum_name}")
hashes: dict[str, str] = {}
for raw_line in path.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line:
continue
match = CHECKSUM_RE.fullmatch(line)
if not match:
raise ValueError(f"invalid checksum line: {raw_line!r}")
name = Path(match.group(2).strip()).name
if name in hashes:
raise ValueError(f"duplicate checksum entry: {name}")
hashes[name] = match.group(1).lower()
required = {pattern.format(version=version) for pattern in CLI_ASSETS.values()}
if set(hashes) != required | {
f"gonavi-cli_{version}_darwin_amd64.tar.gz",
f"gonavi-cli_{version}_darwin_arm64.tar.gz",
f"gonavi-cli_{version}_linux_amd64.tar.gz",
f"gonavi-cli_{version}_linux_arm64.tar.gz",
}:
raise ValueError("checksum file does not contain exactly the six CLI archives")
return hashes
def render_manifest(version: str, hashes: dict[str, str], repo: str) -> str:
lines = [
"# Generated by tools/generate-winget-cli-manifest.py; do not edit hashes by hand.",
"PackageIdentifier: Syngnat.GoNavi.CLI",
f"PackageVersion: {version}",
"PackageLocale: en-US",
"Publisher: Syngnat",
"PublisherUrl: https://github.com/Syngnat",
"PublisherSupportUrl: https://github.com/Syngnat/GoNavi/issues",
"PackageName: GoNavi CLI",
"PackageUrl: https://github.com/Syngnat/GoNavi",
"License: Apache-2.0",
"LicenseUrl: https://github.com/Syngnat/GoNavi/blob/main/LICENSE",
"ShortDescription: Headless GoNavi database CLI",
"Description: Run GoNavi queries, exports, batches, audit exports, and MCP without the desktop GUI.",
"ReleaseNotesUrl: https://github.com/Syngnat/GoNavi/releases/tag/v" + version,
"Installers:",
]
for architecture, pattern in CLI_ASSETS.items():
asset = pattern.format(version=version)
lines.extend(
[
f"- Architecture: {architecture}",
" InstallerType: zip",
" NestedInstallerType: portable",
f" InstallerUrl: https://github.com/{repo}/releases/download/v{version}/{asset}",
f" InstallerSha256: {hashes[asset]}",
" NestedInstallerFiles:",
f" - RelativeFilePath: {BINARY_NAME}",
" PortableCommandAlias: gonavi",
]
)
lines.extend(
[
"ManifestType: singleton",
"ManifestVersion: 1.9.0",
"",
]
)
return "\n".join(lines)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--version", required=True, help="stable semantic version, for example 0.9.3")
parser.add_argument("--checksums", required=True, type=Path, help="independent CLI checksum file")
parser.add_argument("--output", required=True, type=Path)
parser.add_argument("--repo", default="Syngnat/GoNavi")
args = parser.parse_args()
version = args.version.strip().removeprefix("v")
if not VERSION_RE.fullmatch(version):
parser.error(f"invalid stable version: {args.version}")
if not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", args.repo):
parser.error(f"invalid GitHub repository: {args.repo}")
try:
hashes = load_checksums(args.checksums, version)
payload = render_manifest(version, hashes, args.repo)
except (OSError, ValueError) as error:
parser.error(str(error))
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(payload, encoding="utf-8")
print(f"wrote {args.output} for GoNavi CLI {version}")
return 0
if __name__ == "__main__":
raise SystemExit(main())