84 lines
4.1 KiB
Python
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"]
|