#!/usr/bin/env python3 """Create deterministic zip archives. Usage: zip_deterministic.py The archive will include the root directory itself and all files under it. """ from __future__ import annotations import os import sys import time import zipfile from pathlib import Path def _timestamp() -> tuple[int, int, int, int, int, int]: epoch = os.environ.get("SOURCE_DATE_EPOCH") if epoch: try: value = int(epoch) return time.gmtime(value)[:6] except Exception: pass return (2000, 1, 1, 0, 0, 0) def _iter_files(root: Path): for dirpath, dirnames, filenames in os.walk(root): dirnames[:] = sorted([d for d in dirnames if d != "__pycache__"]) for filename in sorted(filenames): if filename.endswith(".pyc"): continue yield Path(dirpath) / filename def _add_file(zf: zipfile.ZipFile, file_path: Path, arcname: str) -> None: info = zipfile.ZipInfo(arcname, date_time=_timestamp()) info.compress_type = zipfile.ZIP_DEFLATED info.external_attr = (0o644 & 0xFFFF) << 16 with file_path.open("rb") as handle: data = handle.read() zf.writestr(info, data, compress_type=zipfile.ZIP_DEFLATED) def main() -> int: if len(sys.argv) != 3: print("Usage: zip_deterministic.py ") return 2 zip_path = Path(sys.argv[1]).resolve() root = Path(sys.argv[2]).resolve() if not root.exists() or not root.is_dir(): print(f"Missing root dir: {root}") return 2 base = root.parent zip_path.parent.mkdir(parents=True, exist_ok=True) if zip_path.exists(): zip_path.unlink() with zipfile.ZipFile(zip_path, "w") as zf: for file_path in sorted(_iter_files(root)): arcname = str(file_path.relative_to(base)).replace(os.sep, "/") _add_file(zf, file_path, arcname) print(str(zip_path)) return 0 if __name__ == "__main__": raise SystemExit(main())