"""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}")