Files
matrix-screen-controller/核桃派软件源代码/app/ota/policy.py
T

84 lines
4.1 KiB
Python

"""Ordered dependency checkpoints, independent of removable release artifacts."""
from __future__ import annotations
import hashlib
import json
from pathlib import Path
from .versioning import SoftwareVersion
BRIDGE_VERSION = SoftwareVersion(1, 1, 0)
def check_upgrade(current: SoftwareVersion, target: SoftwareVersion) -> None:
if target <= current:
raise ValueError(f"OTA target {target} must be newer than installed version {current}")
if current.major != target.major:
raise ValueError("跨主版本升级尚未登记,请使用明确支持此路径的安装包")
if target.minor > current.minor:
required = SoftwareVersion(current.major, current.minor + 1, 0)
if target != required:
raise ValueError(f"请先安装 {required} 软件安装包,不能跳过必经升级版本")
def package_format(version: SoftwareVersion) -> int:
return 1 if version <= BRIDGE_VERSION else 2
def release_metadata(version: SoftwareVersion) -> dict:
checkpoint = version.patch == 0 and version >= BRIDGE_VERSION
predecessor = None
if version >= BRIDGE_VERSION:
predecessor = str(SoftwareVersion(version.major, version.minor - 1 if checkpoint else version.minor, 0))
return {"package_kind": "software-install" if checkpoint else "application",
"required_checkpoint": predecessor, "is_checkpoint": checkpoint}
def read_policy(source: Path) -> dict:
value = json.loads((source / "UPGRADE_POLICY.json").read_text(encoding="utf-8"))
if not isinstance(value, dict) or set(value) != {"schema_version", "checkpoints"} or value["schema_version"] != 1:
raise ValueError("invalid upgrade policy")
previous = SoftwareVersion(1, 0, 0)
for entry in value["checkpoints"]:
version = SoftwareVersion.parse(entry["version"])
if version != SoftwareVersion(previous.major, previous.minor + 1, 0) or not entry["components"]:
raise ValueError("dependency checkpoints must be consecutive minor .0 versions")
for name, component in entry["components"].items():
if name != "frpc" or set(component) != {"version", "sha256", "bundle"}:
raise ValueError("component installer is not registered")
if len(component["sha256"]) != 64 or any(c not in "0123456789abcdef" for c in component["sha256"]):
raise ValueError("invalid component digest")
bundle = Path(component["bundle"])
if bundle.is_absolute() or ".." in bundle.parts or "\\" in component["bundle"]:
raise ValueError("unsafe component bundle path")
previous = version
return value
def required_components(source: Path, version: SoftwareVersion) -> dict:
components = {}
for entry in read_policy(source)["checkpoints"]:
if SoftwareVersion.parse(entry["version"]) <= version:
components.update(entry["components"])
return components
def export_bundle(source: Path, dependency_root: Path, current: SoftwareVersion, target: SoftwareVersion) -> Path | None:
check_upgrade(current, target)
policy = read_policy(source)
if policy["checkpoints"] and target < SoftwareVersion.parse(policy["checkpoints"][-1]["version"]):
raise ValueError("源码已引入新系统依赖;必须先导出已登记的 minor .0 软件安装包")
matching = [e for e in policy["checkpoints"] if e["version"] == str(target)]
if target.patch == 0 and not matching:
raise ValueError("软件安装包必须登记本次依赖变化")
components = required_components(source, target)
# Verify the actual offline source, including on patch exports: silently replacing
# a binary at the same version must not bypass a dependency checkpoint.
for component in components.values():
binary = dependency_root / component["bundle"] / "frpc"
if hashlib.sha256(binary.read_bytes()).hexdigest() != component["sha256"]:
raise ValueError("系统依赖发生变化;请登记新的 minor .0 软件安装版本")
if not matching:
return None
return dependency_root / matching[0]["components"]["frpc"]["bundle"]