"""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"]