149 lines
4.6 KiB
Python
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())
|