Files
ViewIT/scripts/verify_repo_artifacts.py

148 lines
5.2 KiB
Python
Executable File

#!/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())