dev: harden resolver bootstrap and simplify update settings
This commit is contained in:
147
scripts/verify_repo_artifacts.py
Executable file
147
scripts/verify_repo_artifacts.py
Executable file
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate Kodi repository artifacts for ViewIT.
|
||||
|
||||
Usage:
|
||||
verify_repo_artifacts.py <repo_dir> [--expect-branch <branch>]
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
PLUGIN_ID = "plugin.video.viewit"
|
||||
REPO_ID = "repository.viewit"
|
||||
|
||||
|
||||
def _find_addon(root: ET.Element, addon_id: str) -> ET.Element:
|
||||
if root.tag == "addon" and (root.attrib.get("id") or "") == addon_id:
|
||||
return root
|
||||
for addon in root.findall("addon"):
|
||||
if (addon.attrib.get("id") or "") == addon_id:
|
||||
return addon
|
||||
raise ValueError(f"addon {addon_id} not found in addons.xml")
|
||||
|
||||
|
||||
def _read_zip_addon_version(zip_path: Path, addon_id: str) -> str:
|
||||
inner_path = f"{addon_id}/addon.xml"
|
||||
with zipfile.ZipFile(zip_path, "r") as archive:
|
||||
try:
|
||||
data = archive.read(inner_path)
|
||||
except KeyError as exc:
|
||||
raise ValueError(f"{zip_path.name}: missing {inner_path}") from exc
|
||||
root = ET.fromstring(data.decode("utf-8", errors="replace"))
|
||||
version = (root.attrib.get("version") or "").strip()
|
||||
if not version:
|
||||
raise ValueError(f"{zip_path.name}: addon.xml without version")
|
||||
return version
|
||||
|
||||
|
||||
def _check_md5(repo_dir: Path) -> list[str]:
|
||||
errors: list[str] = []
|
||||
addons_xml = repo_dir / "addons.xml"
|
||||
md5_file = repo_dir / "addons.xml.md5"
|
||||
if not addons_xml.exists() or not md5_file.exists():
|
||||
return errors
|
||||
expected = md5_file.read_text(encoding="ascii", errors="ignore").strip().lower()
|
||||
actual = hashlib.md5(addons_xml.read_bytes()).hexdigest()
|
||||
if expected != actual:
|
||||
errors.append("addons.xml.md5 does not match addons.xml")
|
||||
return errors
|
||||
|
||||
|
||||
def _check_repo_zip_branch(zip_path: Path, expected_branch: str) -> list[str]:
|
||||
errors: list[str] = []
|
||||
inner_path = f"{REPO_ID}/addon.xml"
|
||||
with zipfile.ZipFile(zip_path, "r") as archive:
|
||||
try:
|
||||
data = archive.read(inner_path)
|
||||
except KeyError as exc:
|
||||
raise ValueError(f"{zip_path.name}: missing {inner_path}") from exc
|
||||
root = ET.fromstring(data.decode("utf-8", errors="replace"))
|
||||
info = root.find(".//dir/info")
|
||||
if info is None or not (info.text or "").strip():
|
||||
errors.append(f"{zip_path.name}: missing repository info URL")
|
||||
return errors
|
||||
info_url = (info.text or "").strip()
|
||||
marker = f"/branch/{expected_branch}/addons.xml"
|
||||
if marker not in info_url:
|
||||
errors.append(f"{zip_path.name}: info URL does not point to branch '{expected_branch}'")
|
||||
return errors
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("repo_dir", help="Path to repository root (contains addons.xml)")
|
||||
parser.add_argument("--expect-branch", default="", help="Expected branch in repository.viewit addon.xml URL")
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_dir = Path(args.repo_dir).resolve()
|
||||
addons_xml = repo_dir / "addons.xml"
|
||||
if not addons_xml.exists():
|
||||
print(f"Missing: {addons_xml}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
errors: list[str] = []
|
||||
try:
|
||||
root = ET.parse(addons_xml).getroot()
|
||||
plugin_node = _find_addon(root, PLUGIN_ID)
|
||||
repo_node = _find_addon(root, REPO_ID)
|
||||
except Exception as exc:
|
||||
print(f"Invalid addons.xml: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
plugin_version = (plugin_node.attrib.get("version") or "").strip()
|
||||
repo_version = (repo_node.attrib.get("version") or "").strip()
|
||||
if not plugin_version:
|
||||
errors.append("plugin.video.viewit has no version in addons.xml")
|
||||
if not repo_version:
|
||||
errors.append("repository.viewit has no version in addons.xml")
|
||||
|
||||
plugin_zip = repo_dir / PLUGIN_ID / f"{PLUGIN_ID}-{plugin_version}.zip"
|
||||
repo_zip = repo_dir / REPO_ID / f"{REPO_ID}-{repo_version}.zip"
|
||||
if not plugin_zip.exists():
|
||||
errors.append(f"Missing plugin zip: {plugin_zip}")
|
||||
if not repo_zip.exists():
|
||||
errors.append(f"Missing repository zip: {repo_zip}")
|
||||
|
||||
if plugin_zip.exists():
|
||||
try:
|
||||
zip_version = _read_zip_addon_version(plugin_zip, PLUGIN_ID)
|
||||
if zip_version != plugin_version:
|
||||
errors.append(
|
||||
f"{plugin_zip.name}: version mismatch (zip={zip_version}, addons.xml={plugin_version})"
|
||||
)
|
||||
except Exception as exc:
|
||||
errors.append(str(exc))
|
||||
|
||||
if repo_zip.exists():
|
||||
try:
|
||||
zip_version = _read_zip_addon_version(repo_zip, REPO_ID)
|
||||
if zip_version != repo_version:
|
||||
errors.append(f"{repo_zip.name}: version mismatch (zip={zip_version}, addons.xml={repo_version})")
|
||||
if args.expect_branch:
|
||||
errors.extend(_check_repo_zip_branch(repo_zip, args.expect_branch))
|
||||
except Exception as exc:
|
||||
errors.append(str(exc))
|
||||
|
||||
errors.extend(_check_md5(repo_dir))
|
||||
|
||||
if errors:
|
||||
print("Repository validation failed:")
|
||||
for line in errors:
|
||||
print(f"- {line}")
|
||||
return 1
|
||||
|
||||
print("Repository validation passed.")
|
||||
print(f"- plugin: {plugin_version}")
|
||||
print(f"- repository: {repo_version}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user