"""Static content and metadata validation for built orisys wheels. Everything here is pure standard library: the wheel is opened as a zip and inspected in place. No installation, no network, no hardware. The checks encode the guarantees a Nuitka-compiled release must satisfy: * the filename, ``METADATA`` and ``RECORD`` agree on name/version/tags; * PyPI-visible metadata (``Requires-Python``, classifiers, project URLs, license) is present instead of silently dropped by the hand-written ``setup.py`` template in :mod:`wheel.package`; * no Python sources ship inside ``orisys/`` (IP containment) while the compiled extension is present; * every file declared by ``[[tool.orisys.bundle.items]]`` is present and byte-identical to the source tree; * no caches, build leftovers or scratch directories leak in. """ from __future__ import annotations import base64 import hashlib import re import sys import zipfile from dataclasses import dataclass, field from email.parser import Parser from pathlib import Path _BUILD_ROOT = Path(__file__).resolve().parents[1] if str(_BUILD_ROOT) not in sys.path: sys.path.insert(0, str(_BUILD_ROOT)) from wheel.layout import resolve_bundle_source_files # noqa: E402 from wheel.package import is_compliant_manylinux_wheel # noqa: E402 from common.paths import REPO_ROOT # noqa: E402 from common.pyproject_utils import ( # noqa: E402 bundle_items, extras_require, install_requires, ) # PyPI's default per-project file limit is 100 MB; keep headroom. MAX_WHEEL_BYTES = 90 * 1024 * 1024 _FILENAME_RE = re.compile( r"^(?P[^-]+)-(?P[^-]+)-(?P[^-]+)-(?P[^-]+)-(?P.+)\.whl$" ) _COMPILED_EXT_RE = re.compile(r"^orisys/__init__\..+\.(?:pyd|so)$") _FORBIDDEN_PREFIXES = ( "test/", "tests/", "data/", "build/", "dist/", "sdk/", ".git", ) _FORBIDDEN_SUFFIXES = (".pyc", ".pyo", ".c", ".pdb", ".map", ".spec") @dataclass(frozen=True) class Check: name: str ok: bool detail: str = "" def render(self) -> str: return f"[{'ok' if self.ok else 'FAIL'}] {self.name}: {self.detail}" @dataclass class WheelCheckResult: wheel: Path version: str | None = None name: str | None = None platform_tag: str | None = None entry_count: int = 0 uncompressed_bytes: int = 0 file_bytes: int = 0 checks: list[Check] = field(default_factory=list) warnings: list[str] = field(default_factory=list) @property def ok(self) -> bool: return all(check.ok for check in self.checks) @property def failures(self) -> list[Check]: return [check for check in self.checks if not check.ok] def add(self, name: str, ok: bool, detail: str = "") -> bool: self.checks.append(Check(name=name, ok=ok, detail=detail)) return ok def warn(self, message: str) -> None: self.warnings.append(message) def render(self) -> str: lines = [ f"wheel: {self.wheel.name}", f" version={self.version} platform={self.platform_tag} " f"entries={self.entry_count} file_size={self.file_bytes} " f"uncompressed={self.uncompressed_bytes}", ] lines.extend(f" {check.render()}" for check in self.checks) lines.extend(f" [warn] {message}" for message in self.warnings) lines.append(f" RESULT: {'PASS' if self.ok else 'FAIL'}") return "\n".join(lines) def _sha256(data: bytes) -> str: return hashlib.sha256(data).hexdigest() def _requirement_name(spec: str) -> str: return re.split(r"[<>=!~\[; ]", spec, maxsplit=1)[0].strip().lower().replace("_", "-") def _metadata_message(zf: zipfile.ZipFile, result: WheelCheckResult): candidates = [name for name in zf.namelist() if name.endswith(".dist-info/METADATA")] if not candidates: result.add("metadata-present", False, "no *.dist-info/METADATA entry in the wheel") return None raw = zf.read(candidates[0]).decode("utf-8", errors="replace") message = Parser().parsestr(raw) result.add("metadata-present", True, candidates[0]) return message def _check_filename(result: WheelCheckResult, expected_version: str | None) -> None: match = _FILENAME_RE.match(result.wheel.name) if match is None: result.add("filename", False, f"unparsable wheel filename: {result.wheel.name}") return result.name = match.group("name") result.version = match.group("version") result.platform_tag = match.group("platform") problems = [] if result.name != "orisys": problems.append(f"distribution name is {result.name!r}, expected 'orisys'") if expected_version is not None and result.version != expected_version: problems.append(f"filename version {result.version!r} != expected {expected_version!r}") result.add("filename", not problems, "; ".join(problems) or result.wheel.name) def _check_metadata( result: WheelCheckResult, message, expected_version: str | None, repo_root: Path | None, ) -> None: if message is None: return result.add( "metadata-name", message.get("Name") == "orisys", f"Name={message.get('Name')!r}", ) declared = message.get("Version") expected = expected_version or result.version if expected is not None and declared != expected: result.add( "metadata-version", False, f"METADATA Version={declared!r} != expected {expected!r}", ) else: result.add("metadata-version", True, f"Version={declared!r}") requires_python = (message.get("Requires-Python") or "").strip() result.add( "metadata-requires-python", bool(requires_python), f"Requires-Python={requires_python!r}" if requires_python else "Requires-Python is missing", ) classifiers = message.get_all("Classifier") or [] result.add( "metadata-classifiers", bool(classifiers), f"{len(classifiers)} classifier(s)" if classifiers else "no Classifier fields", ) project_urls = message.get_all("Project-URL") or [] result.add( "metadata-project-urls", bool(project_urls), f"{len(project_urls)} Project-URL(s)" if project_urls else "no Project-URL fields", ) license_values = [ (message.get("License") or "").strip(), (message.get("License-Expression") or "").strip(), ] has_license = any(license_values) or any( value.startswith("License ::") for value in classifiers ) result.add( "metadata-license", has_license, f"License={license_values[0]!r} License-Expression={license_values[1]!r}" if has_license else "no License / License-Expression / 'License ::' classifier", ) dynamic = message.get_all("Dynamic") or [] result.add( "metadata-no-dynamic-version", "version" not in dynamic, f"Dynamic={dynamic!r}" if dynamic else "no Dynamic fields", ) if dynamic: result.warn(f"METADATA still declares Dynamic fields: {dynamic!r}") if repo_root is not None and (repo_root / "pyproject.toml").is_file(): requires_dist = message.get_all("Requires-Dist") or [] provided = {_requirement_name(entry) for entry in requires_dist} missing_core = [ spec for spec in install_requires(repo_root) if _requirement_name(spec) not in provided ] result.add( "metadata-requires-dist", not missing_core, f"missing core Requires-Dist entries: {missing_core!r}" if missing_core else f"{len(requires_dist)} Requires-Dist entries", ) extras = message.get_all("Provides-Extra") or [] missing_extras = sorted(set(extras_require(repo_root)) - set(extras)) result.add( "metadata-extras", not missing_extras, f"missing Provides-Extra: {missing_extras!r}" if missing_extras else f"Provides-Extra={sorted(extras)!r}", ) def _check_compiled_only(result: WheelCheckResult, names: list[str]) -> None: compiled = [name for name in names if _COMPILED_EXT_RE.match(name)] result.add( "compiled-extension", bool(compiled), f"{compiled[0]}" if compiled else "no orisys/__init__..pyd|.so entry", ) # orisys/examples/** is shipped as package data on purpose; every other # orisys/**/*.py means source code leaked past the Nuitka compile step. sources = [ name for name in names if name.startswith("orisys/") and name.endswith(".py") and not name.startswith("orisys/examples/") ] result.add( "no-python-sources", not sources, f"python sources leaked into the wheel: {sources[:5]!r}" if sources else "compiled package ships no python sources (examples excluded by design)", ) blend = [name for name in names if name.startswith("orisys_blend/")] result.add( "orisys-blend-present", "orisys_blend/__init__.py" in blend and "orisys_blend/kernel.py" in blend, f"{sorted(blend)!r}", ) def _check_bundle_fidelity( result: WheelCheckResult, zf: zipfile.ZipFile, names: set[str], repo_root: Path | None, strict: bool, ) -> None: if repo_root is None or not (repo_root / "pyproject.toml").is_file(): result.warn("pyproject.toml not found; bundle fidelity checks skipped") return missing: list[str] = [] mismatched: list[str] = [] checked = 0 for item in bundle_items(repo_root): target = str(item["target"]).replace("\\", "/") source_dir = repo_root / item["source"] for source_file in resolve_bundle_source_files(repo_root, item): if item.get("preserve_tree", False): relative = source_file.relative_to(source_dir).as_posix() entry = f"orisys/{target}/{relative}" else: entry = f"orisys/{target}/{source_file.name}" if entry not in names: missing.append(entry) continue checked += 1 if _sha256(zf.read(entry)) != _sha256(source_file.read_bytes()): mismatched.append(entry) # Both entries here mean "the wheel does not match this revision of the # source tree". Against a fresh build that is fatal; against an already # released wheel it is expected, so ``strict`` (set by preflight, unset by # audit) decides whether it fails or warns. if missing: detail = ( f"{len(missing)} declared file(s) absent from the wheel (added to the " f"source tree after this build?): {missing[:5]!r}" ) result.add("bundle-completeness", not strict, detail) if not strict: result.warn(detail) else: result.add( "bundle-completeness", True, f"{checked} declared bundled file(s) present" ) if mismatched: detail = ( f"{len(mismatched)} of {checked} bundled file(s) differ from the source " f"tree (wheel predates or postdates it): {mismatched[:5]!r}" ) result.add("bundle-fidelity", not strict, detail) if not strict: result.warn(detail) else: result.add( "bundle-fidelity", True, f"{checked} bundled file(s) byte-identical to source", ) def _check_forbidden_entries(result: WheelCheckResult, names: list[str]) -> None: offenders = [] for name in names: lowered = name.lower() if lowered.startswith(_FORBIDDEN_PREFIXES) or lowered.endswith(_FORBIDDEN_SUFFIXES) or "__pycache__" in lowered or ".egg-info" in lowered: offenders.append(name) result.add( "no-forbidden-entries", not offenders, f"unexpected entries: {offenders[:5]!r}" if offenders else "no caches/leftovers/scratch paths", ) def _check_archive_paths(result: WheelCheckResult, names: list[str]) -> None: unsafe = [ name for name in names if name.startswith(("/", "\\")) or ".." in Path(name.replace("\\", "/")).parts ] result.add( "archive-paths", not unsafe, f"absolute or traversing entries: {unsafe[:5]!r}" if unsafe else "all entries are relative", ) def _check_record(result: WheelCheckResult, zf: zipfile.ZipFile, names: list[str]) -> None: records = [name for name in names if name.endswith(".dist-info/RECORD")] if not records: result.add("record", False, "no *.dist-info/RECORD entry") return bad_paths: list[str] = [] bad_hashes: list[str] = [] bad_sizes: list[str] = [] present = set(names) for line in zf.read(records[0]).decode("utf-8").splitlines(): if not line.strip(): continue parts = line.split(",") while len(parts) < 3: parts.append("") path, hash_field, size_field = parts[0], parts[1], parts[2] if path not in present: bad_paths.append(path) continue if not hash_field.startswith("sha256="): continue expected = hash_field[len("sha256=") :] actual = base64.urlsafe_b64encode( hashlib.sha256(zf.read(path)).digest() ).rstrip(b"=").decode("ascii") if actual != expected: bad_hashes.append(path) if size_field and size_field.isdigit(): if int(size_field) != len(zf.read(path)): bad_sizes.append(path) problems = [] if bad_paths: problems.append(f"paths not in archive: {bad_paths[:5]!r}") if bad_hashes: problems.append(f"sha256 mismatch: {bad_hashes[:5]!r}") if bad_sizes: problems.append(f"size mismatch: {bad_sizes[:5]!r}") result.add( "record", not problems, "; ".join(problems) if problems else f"{records[0]} verifies", ) def _check_platform_tag(result: WheelCheckResult) -> None: tag = result.platform_tag if tag is None: return if "win" in tag: ok = tag == "win_amd64" result.add("platform-tag", ok, f"{tag!r} (expected 'win_amd64')") return ok = is_compliant_manylinux_wheel(result.wheel) result.add( "platform-tag", ok, f"{tag!r}" if ok else f"{tag!r} is not a PyPI-accepted manylinux x86_64 tag", ) def _check_size(result: WheelCheckResult, max_bytes: int) -> None: result.add( "size-limit", result.file_bytes <= max_bytes, f"{result.file_bytes} bytes (limit {max_bytes})", ) def inspect_wheel( wheel_path: Path, *, expected_version: str | None = None, repo_root: Path | None = None, max_bytes: int = MAX_WHEEL_BYTES, strict_bundle: bool = True, ) -> WheelCheckResult: """Validate one built wheel without installing it. ``strict_bundle`` controls how the wheel's bundled files are compared with the current source tree: ``True`` (gate a fresh build, used by ``release preflight``) fails on any missing or changed declared file; ``False`` (audit an already-released wheel) reports the same findings as warnings. """ wheel_path = Path(wheel_path).resolve() if repo_root is None: repo_root = REPO_ROOT if (REPO_ROOT / "pyproject.toml").is_file() else None else: repo_root = Path(repo_root).resolve() result = WheelCheckResult(wheel=wheel_path) if not wheel_path.is_file(): result.add("wheel-exists", False, f"not a file: {wheel_path}") return result result.file_bytes = wheel_path.stat().st_size result.add("wheel-exists", True, str(wheel_path)) _check_filename(result, expected_version) _check_platform_tag(result) _check_size(result, max_bytes) try: zf = zipfile.ZipFile(wheel_path) except zipfile.BadZipFile as exc: result.add("zip-integrity", False, f"not a valid zip: {exc}") return result with zf: corrupt = zf.testzip() result.add( "zip-integrity", corrupt is None, "CRC check passed" if corrupt is None else f"corrupt entry: {corrupt}", ) names = zf.namelist() name_set = set(names) result.entry_count = len(names) result.uncompressed_bytes = sum(info.file_size for info in zf.infolist()) _check_archive_paths(result, names) _check_forbidden_entries(result, names) message = _metadata_message(zf, result) _check_metadata(result, message, expected_version, repo_root) _check_compiled_only(result, names) _check_bundle_fidelity(result, zf, name_set, repo_root, strict_bundle) _check_record(result, zf, names) return result