#!/usr/bin/env python3 """Проверка vendor/wheels против requirements-prod.txt (блокирует сборку ZIP при пробелах).""" from __future__ import annotations import argparse import hashlib import json import subprocess import sys from pathlib import Path ROOT = Path(__file__).resolve().parent.parent if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from install_deps import missing_wheelhouse_packages def wheelhouse_stats(wheels_dir: Path) -> tuple[int, int]: """(число .whl/.tar.gz, суммарный размер байт).""" count = 0 total = 0 if not wheels_dir.is_dir(): return 0, 0 for path in wheels_dir.iterdir(): if not path.is_file(): continue if path.name in (".gitkeep", "manifest.json"): continue if path.suffix not in (".whl", ".gz", ".zip"): continue count += 1 total += path.stat().st_size return count, total def verify_offline_closure(req_file: Path, wheels_dir: Path, python_exe: str) -> None: """pip install --dry-run только из wheelhouse (все транзитивные deps).""" cmd = [ python_exe, "-m", "pip", "install", "--no-index", f"--find-links={wheels_dir}", "-r", str(req_file), "--dry-run", "--ignore-installed", ] subprocess.run(cmd, check=True, capture_output=True, text=True) def write_manifest( req_file: Path, wheels_dir: Path, manifest_path: Path, *, build_arch: str = "", build_platform: str = "", ) -> None: req_hash = hashlib.sha256(req_file.read_bytes()).hexdigest() if req_file.is_file() else "" whl_count, whl_bytes = wheelhouse_stats(wheels_dir) wheels = sorted( p.name for p in wheels_dir.glob("*") if p.is_file() and p.name not in (".gitkeep", "manifest.json") ) payload = { "requirements_sha256": req_hash, "wheel_count": whl_count, "wheelhouse_bytes": whl_bytes, "wheels": wheels, } if build_arch: payload["build_arch"] = build_arch if build_platform: payload["build_platform"] = build_platform manifest_path.write_text( json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", ) def format_bytes(n: int) -> str: if n >= 1024 * 1024: return f"{n / (1024 * 1024):.1f} MB" if n >= 1024: return f"{n / 1024:.0f} KB" return f"{n} B" def main() -> int: parser = argparse.ArgumentParser(description="Verify vendor/wheels for release ZIP") parser.add_argument("--req", type=Path, default=ROOT / "requirements-prod.txt") parser.add_argument("--wheels", type=Path, default=ROOT / "vendor" / "wheels") parser.add_argument("--python", default=sys.executable, help="Python для pip dry-run") parser.add_argument("--write-manifest", action="store_true") parser.add_argument("--verify-closure", action="store_true", help="pip dry-run offline install") parser.add_argument("--build-arch", default="", help="Архитектура build-машины (uname -m)") parser.add_argument("--build-platform", default="", help="ОС build-машины") args = parser.parse_args() missing = missing_wheelhouse_packages(args.req, args.wheels) whl_count, whl_bytes = wheelhouse_stats(args.wheels) if args.write_manifest and not missing: write_manifest( args.req, args.wheels, args.wheels / "manifest.json", build_arch=args.build_arch.strip(), build_platform=args.build_platform.strip(), ) if missing: print("verify_release_deps: missing wheels for:", file=sys.stderr) for m in missing: print(f" - {m}", file=sys.stderr) return 1 if whl_count == 0: print("verify_release_deps: wheelhouse empty", file=sys.stderr) return 1 print( f"verify_release_deps: OK — {whl_count} packages, " f"{format_bytes(whl_bytes)} in {args.wheels}" ) if args.verify_closure: try: verify_offline_closure(args.req, args.wheels, args.python) except subprocess.CalledProcessError as exc: print("verify_release_deps: offline pip closure FAILED:", file=sys.stderr) if exc.stderr: print(exc.stderr, file=sys.stderr) if exc.stdout: print(exc.stdout, file=sys.stderr) return 1 print("verify_release_deps: offline pip closure OK (all deps from wheelhouse)") return 0 if __name__ == "__main__": raise SystemExit(main())