Add validated OTA candidate publishing and fix 1.1.2 dependency failure
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
"""Standard-library preflight for the device's offline Python requirements."""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
|
||||
_REQUIREMENT = re.compile(r"^([A-Za-z0-9_.-]+)(?:\[[A-Za-z0-9_,.-]+\])?(.*)$")
|
||||
_DIGEST_LINE = re.compile(r"^([0-9a-f]{64}) ([^/\\]+\.whl)$")
|
||||
|
||||
|
||||
def _normalized(name: str) -> str:
|
||||
return re.sub(r"[-_.]+", "-", name).lower()
|
||||
|
||||
|
||||
def _requirements(source: Path, path: Path, visited: set[Path]) -> list[tuple[str, str]]:
|
||||
path = path.resolve()
|
||||
try:
|
||||
path.relative_to(source.resolve())
|
||||
except ValueError as exc:
|
||||
raise ValueError("requirement include escapes the source directory") from exc
|
||||
if path in visited:
|
||||
return []
|
||||
visited.add(path)
|
||||
result: list[tuple[str, str]] = []
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
item = line.split("#", 1)[0].strip()
|
||||
if not item:
|
||||
continue
|
||||
if item.startswith("-r "):
|
||||
result.extend(_requirements(source, path.parent / item[3:].strip(), visited))
|
||||
continue
|
||||
match = _REQUIREMENT.fullmatch(item)
|
||||
if match is None:
|
||||
raise ValueError(f"unsupported device requirement in {path.name}")
|
||||
name, specifier = match.groups()
|
||||
if specifier and not specifier.startswith(("==", ">=", "<=", ">", "<", "!=", "~=")):
|
||||
raise ValueError(f"unsupported device requirement in {path.name}")
|
||||
result.append((_normalized(name), specifier))
|
||||
return result
|
||||
|
||||
|
||||
def validate_offline_wheels(source: Path, wheelhouse: Path) -> None:
|
||||
"""Reject a missing direct dependency or inconsistent wheel SHA-256 list.
|
||||
|
||||
Full transitive and platform resolution remains a separate offline pip gate
|
||||
against the extracted candidate, followed by the board's actual venv install.
|
||||
"""
|
||||
source = Path(source).resolve()
|
||||
wheelhouse = Path(wheelhouse).resolve()
|
||||
requirements = _requirements(source, source / "requirements-dev.txt", set())
|
||||
listed: dict[str, str] = {}
|
||||
for line in (wheelhouse / "SHA256SUMS").read_text(encoding="ascii").splitlines():
|
||||
match = _DIGEST_LINE.fullmatch(line)
|
||||
if match is None or match[2] in listed:
|
||||
raise ValueError("offline wheel SHA256SUMS contains an invalid or repeated entry")
|
||||
listed[match[2]] = match[1]
|
||||
actual = {path.name for path in wheelhouse.iterdir() if path.is_file() and path.suffix == ".whl"}
|
||||
if not listed or actual != set(listed):
|
||||
raise ValueError("offline wheel files and SHA256SUMS do not match")
|
||||
packages: dict[str, set[str]] = {}
|
||||
for filename, expected in listed.items():
|
||||
parts = filename.removesuffix(".whl").split("-")
|
||||
if len(parts) < 5:
|
||||
raise ValueError(f"invalid offline wheel filename: {filename}")
|
||||
name, version = _normalized(parts[0]), parts[1]
|
||||
packages.setdefault(name, set()).add(version)
|
||||
digest = hashlib.sha256()
|
||||
with (wheelhouse / filename).open("rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
if digest.hexdigest() != expected:
|
||||
raise ValueError(f"offline wheel digest differs from SHA256SUMS: {filename}")
|
||||
for name, specifier in requirements:
|
||||
versions = packages.get(name, set())
|
||||
if not versions:
|
||||
raise ValueError(f"offline wheel missing for direct requirement: {name}")
|
||||
if specifier.startswith("==") and specifier[2:] not in versions:
|
||||
raise ValueError(f"offline wheel version missing for direct requirement: {name}{specifier}")
|
||||
Reference in New Issue
Block a user