Files
site/WESP_REL/scripts/verify_release_deps.py
2026-07-17 12:57:18 +03:00

149 lines
4.6 KiB
Python

#!/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())