| """Build hook that ships an integrity manifest inside the wheel. |
| |
| The manifest hashes every packaged file. At runtime the package recomputes those |
| hashes and refuses to run if any differ. The manifest is written to a temporary |
| path and force-included, so the source tree stays clean and editable installs |
| remain unenforced. |
| """ |
|
|
| |
|
|
| from __future__ import annotations |
|
|
| import hashlib |
| import json |
| import tempfile |
| from pathlib import Path |
| from typing import Any |
|
|
| from hatchling.builders.hooks.plugin.interface import BuildHookInterface |
|
|
| PACKAGE = "affix_huggingface" |
| MANIFEST_NAME = "_integrity_manifest.json" |
| ALGORITHM = "sha256" |
| _SKIP_SUFFIXES = (".pyc", ".pyo") |
| _SKIP_DIRS = {"__pycache__"} |
|
|
|
|
| def _hash_file(path: Path) -> str: |
| digest = hashlib.new(ALGORITHM) |
| with path.open("rb") as handle: |
| for chunk in iter(lambda: handle.read(65536), b""): |
| digest.update(chunk) |
| return digest.hexdigest() |
|
|
|
|
| class CustomBuildHook(BuildHookInterface): |
| PLUGIN_NAME = "custom" |
|
|
| def initialize(self, version: str, build_data: dict[str, Any]) -> None: |
| if self.target_name != "wheel": |
| return |
|
|
| root = Path(self.root) / "src" / PACKAGE |
| files: dict[str, str] = {} |
| for path in sorted(root.rglob("*")): |
| if not path.is_file(): |
| continue |
| rel = path.relative_to(root) |
| if any(part in _SKIP_DIRS for part in rel.parts): |
| continue |
| if path.suffix in _SKIP_SUFFIXES: |
| continue |
| if path.name == MANIFEST_NAME: |
| continue |
| files[rel.as_posix()] = _hash_file(path) |
|
|
| manifest = {"version": 1, "algorithm": ALGORITHM, "files": files} |
| tmp = Path(tempfile.mkdtemp(prefix="affix-manifest-")) / MANIFEST_NAME |
| tmp.write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8") |
|
|
| force_include = build_data.setdefault("force_include", {}) |
| force_include[str(tmp)] = f"{PACKAGE}/{MANIFEST_NAME}" |
|
|